From 4740b004958ac8cd482e188c22e456a0f64a8441 Mon Sep 17 00:00:00 2001 From: HsiangNianian Date: Sun, 8 Sep 2024 03:17:45 +0000 Subject: [PATCH] chore(docs): update api docs with sphinx-apidoc --- docs/source/api/GetPlayerCard.rst | 18 + docs/source/api/GetPlayerCard.version.rst | 7 + docs/source/api/HydroRoll.cli.rst | 7 + docs/source/api/HydroRoll.config.rst | 7 + docs/source/api/HydroRoll.exceptions.rst | 7 + docs/source/api/HydroRoll.models.rst | 18 + ...eptions.rst => HydroRoll.models.utils.rst} | 4 +- .../api/{hydro_roll.rst => HydroRoll.rst} | 18 +- docs/source/api/HydroRoll.typing.rst | 7 + ...hydro_roll.cli.rst => HydroRoll.utils.rst} | 4 +- ...o_roll.utils.rst => HydroRollCore.cli.rst} | 4 +- docs/source/api/HydroRollCore.config.rst | 7 + docs/source/api/HydroRollCore.const.rst | 7 + ...roll.config.rst => HydroRollCore.core.rst} | 4 +- .../source/api/HydroRollCore.dependencies.rst | 7 + .../api/HydroRollCore.dev.character.rst | 7 + docs/source/api/HydroRollCore.dev.echo.rst | 7 + docs/source/api/HydroRollCore.dev.grps.rst | 18 + docs/source/api/HydroRollCore.dev.grps.v1.rst | 7 + docs/source/api/HydroRollCore.dev.rst | 27 + docs/source/api/HydroRollCore.doc.rst | 10 + docs/source/api/HydroRollCore.event.rst | 7 + ...utils.rst => HydroRollCore.exceptions.rst} | 4 +- docs/source/api/HydroRollCore.feat.rst | 10 + docs/source/api/HydroRollCore.log.rst | 7 + docs/source/api/HydroRollCore.perf.rst | 10 + docs/source/api/HydroRollCore.rst | 39 ++ ...roRollCore.rule.BaseRule.CharacterCard.rst | 7 + ...HydroRollCore.rule.BaseRule.CustomRule.rst | 7 + .../api/HydroRollCore.rule.BaseRule.Wiki.rst | 7 + .../api/HydroRollCore.rule.BaseRule.rst | 20 + ...roll.models.rst => HydroRollCore.rule.rst} | 10 +- docs/source/api/HydroRollCore.typing.rst | 7 + docs/source/api/HydroRollCore.utils.rst | 7 + docs/source/api/OneRoll.cli.rst | 7 + docs/source/api/OneRoll.dice.rst | 7 + docs/source/api/OneRoll.diceast.rst | 7 + docs/source/api/OneRoll.errors.rst | 7 + ...roll.typing.rst => OneRoll.expression.rst} | 4 +- docs/source/api/OneRoll.rst | 24 + docs/source/api/OneRoll.stringifiers.rst | 7 + docs/source/api/OneRoll.utils.rst | 7 + docs/source/api/TRPGNivisSDK.exception.rst | 7 + docs/source/api/TRPGNivisSDK.execution.rst | 7 + docs/source/api/TRPGNivisSDK.interpreter.rst | 7 + docs/source/api/TRPGNivisSDK.lexer.rst | 7 + docs/source/api/TRPGNivisSDK.mathmatics.rst | 7 + docs/source/api/TRPGNivisSDK.parsers.rst | 7 + docs/source/api/TRPGNivisSDK.rst | 24 + docs/source/api/TRPGNivisSDK.type.rst | 7 + docs/source/api/index.rst | 10 +- modules/GetPlayerCard/__init__.py | 0 modules/GetPlayerCard/css/option_list.tcss | 8 + .../GetPlayerCard/templates/tests.template | 1 + modules/GetPlayerCard/version.py | 3 + modules/HydroRoll/__init__.py | 80 +++ modules/HydroRoll/cli.py | 186 +++++ modules/HydroRoll/config.py | 96 +++ modules/HydroRoll/exceptions.py | 0 modules/HydroRoll/models/__init__.py | 0 modules/HydroRoll/models/hola.pkl | Bin 0 -> 209 bytes modules/HydroRoll/models/utils.py | 15 + modules/HydroRoll/typing.py | 34 + modules/HydroRoll/utils.py | 162 +++++ modules/HydroRollCore/LibCore.pyi | 5 + modules/HydroRollCore/__init__.py | 13 + modules/HydroRollCore/cli.py | 46 ++ modules/HydroRollCore/config.py | 29 + modules/HydroRollCore/const.py | 0 modules/HydroRollCore/core.py | 638 ++++++++++++++++++ modules/HydroRollCore/dependencies.py | 118 ++++ modules/HydroRollCore/dev/__init__.py | 1 + modules/HydroRollCore/dev/character.py | 2 + modules/HydroRollCore/dev/echo.py | 15 + modules/HydroRollCore/dev/grps/__init__.py | 1 + modules/HydroRollCore/dev/grps/v1.py | 1 + modules/HydroRollCore/doc/__init__.py | 0 modules/HydroRollCore/event.py | 108 +++ modules/HydroRollCore/exceptions.py | 22 + modules/HydroRollCore/feat/__init__.py | 0 modules/HydroRollCore/log.py | 25 + modules/HydroRollCore/perf/__init__.py | 0 modules/HydroRollCore/py.typed | 0 .../rule/BaseRule/CharacterCard.py | 17 + .../HydroRollCore/rule/BaseRule/CustomRule.py | 0 modules/HydroRollCore/rule/BaseRule/Wiki.py | 0 .../HydroRollCore/rule/BaseRule/__init__.py | 3 + modules/HydroRollCore/rule/__init__.py | 164 +++++ modules/HydroRollCore/typing.py | 20 + modules/HydroRollCore/utils.py | 291 ++++++++ modules/OneRoll/__init__.py | 14 + modules/OneRoll/cli.py | 19 + modules/OneRoll/dice.py | 276 ++++++++ modules/OneRoll/diceast.py | 452 +++++++++++++ modules/OneRoll/errors.py | 32 + modules/OneRoll/expression.py | 634 +++++++++++++++++ modules/OneRoll/grammar.lark | 60 ++ modules/OneRoll/stringifiers.py | 198 ++++++ modules/OneRoll/utils.py | 221 ++++++ modules/TRPGNivisSDK/Grammar/Token | 55 ++ .../TRPGNivisSDK/Lib/IOStream/__init__.nivis | 1 + .../TRPGNivisSDK/Modules/asyncio/__init__.py | 0 modules/TRPGNivisSDK/__init__.py | 12 + modules/TRPGNivisSDK/exception.py | 37 + modules/TRPGNivisSDK/execution.py | 47 ++ modules/TRPGNivisSDK/interpreter.py | 76 +++ modules/TRPGNivisSDK/lexer.py | 276 ++++++++ modules/TRPGNivisSDK/mathmatics.py | 0 modules/TRPGNivisSDK/parsers.py | 145 ++++ modules/TRPGNivisSDK/type.py | 0 110 files changed, 5127 insertions(+), 29 deletions(-) create mode 100644 docs/source/api/GetPlayerCard.rst create mode 100644 docs/source/api/GetPlayerCard.version.rst create mode 100644 docs/source/api/HydroRoll.cli.rst create mode 100644 docs/source/api/HydroRoll.config.rst create mode 100644 docs/source/api/HydroRoll.exceptions.rst create mode 100644 docs/source/api/HydroRoll.models.rst rename docs/source/api/{hydro_roll.exceptions.rst => HydroRoll.models.utils.rst} (55%) rename docs/source/api/{hydro_roll.rst => HydroRoll.rst} (50%) create mode 100644 docs/source/api/HydroRoll.typing.rst rename docs/source/api/{hydro_roll.cli.rst => HydroRoll.utils.rst} (58%) rename docs/source/api/{hydro_roll.utils.rst => HydroRollCore.cli.rst} (57%) create mode 100644 docs/source/api/HydroRollCore.config.rst create mode 100644 docs/source/api/HydroRollCore.const.rst rename docs/source/api/{hydro_roll.config.rst => HydroRollCore.core.rst} (57%) create mode 100644 docs/source/api/HydroRollCore.dependencies.rst create mode 100644 docs/source/api/HydroRollCore.dev.character.rst create mode 100644 docs/source/api/HydroRollCore.dev.echo.rst create mode 100644 docs/source/api/HydroRollCore.dev.grps.rst create mode 100644 docs/source/api/HydroRollCore.dev.grps.v1.rst create mode 100644 docs/source/api/HydroRollCore.dev.rst create mode 100644 docs/source/api/HydroRollCore.doc.rst create mode 100644 docs/source/api/HydroRollCore.event.rst rename docs/source/api/{hydro_roll.models.utils.rst => HydroRollCore.exceptions.rst} (54%) create mode 100644 docs/source/api/HydroRollCore.feat.rst create mode 100644 docs/source/api/HydroRollCore.log.rst create mode 100644 docs/source/api/HydroRollCore.perf.rst create mode 100644 docs/source/api/HydroRollCore.rst create mode 100644 docs/source/api/HydroRollCore.rule.BaseRule.CharacterCard.rst create mode 100644 docs/source/api/HydroRollCore.rule.BaseRule.CustomRule.rst create mode 100644 docs/source/api/HydroRollCore.rule.BaseRule.Wiki.rst create mode 100644 docs/source/api/HydroRollCore.rule.BaseRule.rst rename docs/source/api/{hydro_roll.models.rst => HydroRollCore.rule.rst} (55%) create mode 100644 docs/source/api/HydroRollCore.typing.rst create mode 100644 docs/source/api/HydroRollCore.utils.rst create mode 100644 docs/source/api/OneRoll.cli.rst create mode 100644 docs/source/api/OneRoll.dice.rst create mode 100644 docs/source/api/OneRoll.diceast.rst create mode 100644 docs/source/api/OneRoll.errors.rst rename docs/source/api/{hydro_roll.typing.rst => OneRoll.expression.rst} (57%) create mode 100644 docs/source/api/OneRoll.rst create mode 100644 docs/source/api/OneRoll.stringifiers.rst create mode 100644 docs/source/api/OneRoll.utils.rst create mode 100644 docs/source/api/TRPGNivisSDK.exception.rst create mode 100644 docs/source/api/TRPGNivisSDK.execution.rst create mode 100644 docs/source/api/TRPGNivisSDK.interpreter.rst create mode 100644 docs/source/api/TRPGNivisSDK.lexer.rst create mode 100644 docs/source/api/TRPGNivisSDK.mathmatics.rst create mode 100644 docs/source/api/TRPGNivisSDK.parsers.rst create mode 100644 docs/source/api/TRPGNivisSDK.rst create mode 100644 docs/source/api/TRPGNivisSDK.type.rst create mode 100644 modules/GetPlayerCard/__init__.py create mode 100644 modules/GetPlayerCard/css/option_list.tcss create mode 100644 modules/GetPlayerCard/templates/tests.template create mode 100644 modules/GetPlayerCard/version.py create mode 100644 modules/HydroRoll/__init__.py create mode 100644 modules/HydroRoll/cli.py create mode 100644 modules/HydroRoll/config.py create mode 100644 modules/HydroRoll/exceptions.py create mode 100644 modules/HydroRoll/models/__init__.py create mode 100644 modules/HydroRoll/models/hola.pkl create mode 100644 modules/HydroRoll/models/utils.py create mode 100644 modules/HydroRoll/typing.py create mode 100644 modules/HydroRoll/utils.py create mode 100644 modules/HydroRollCore/LibCore.pyi create mode 100644 modules/HydroRollCore/__init__.py create mode 100644 modules/HydroRollCore/cli.py create mode 100644 modules/HydroRollCore/config.py create mode 100644 modules/HydroRollCore/const.py create mode 100644 modules/HydroRollCore/core.py create mode 100644 modules/HydroRollCore/dependencies.py create mode 100644 modules/HydroRollCore/dev/__init__.py create mode 100644 modules/HydroRollCore/dev/character.py create mode 100644 modules/HydroRollCore/dev/echo.py create mode 100644 modules/HydroRollCore/dev/grps/__init__.py create mode 100644 modules/HydroRollCore/dev/grps/v1.py create mode 100644 modules/HydroRollCore/doc/__init__.py create mode 100644 modules/HydroRollCore/event.py create mode 100644 modules/HydroRollCore/exceptions.py create mode 100644 modules/HydroRollCore/feat/__init__.py create mode 100644 modules/HydroRollCore/log.py create mode 100644 modules/HydroRollCore/perf/__init__.py create mode 100644 modules/HydroRollCore/py.typed create mode 100644 modules/HydroRollCore/rule/BaseRule/CharacterCard.py create mode 100644 modules/HydroRollCore/rule/BaseRule/CustomRule.py create mode 100644 modules/HydroRollCore/rule/BaseRule/Wiki.py create mode 100644 modules/HydroRollCore/rule/BaseRule/__init__.py create mode 100644 modules/HydroRollCore/rule/__init__.py create mode 100644 modules/HydroRollCore/typing.py create mode 100644 modules/HydroRollCore/utils.py create mode 100644 modules/OneRoll/__init__.py create mode 100644 modules/OneRoll/cli.py create mode 100644 modules/OneRoll/dice.py create mode 100644 modules/OneRoll/diceast.py create mode 100644 modules/OneRoll/errors.py create mode 100644 modules/OneRoll/expression.py create mode 100644 modules/OneRoll/grammar.lark create mode 100644 modules/OneRoll/stringifiers.py create mode 100644 modules/OneRoll/utils.py create mode 100644 modules/TRPGNivisSDK/Grammar/Token create mode 100644 modules/TRPGNivisSDK/Lib/IOStream/__init__.nivis create mode 100644 modules/TRPGNivisSDK/Modules/asyncio/__init__.py create mode 100644 modules/TRPGNivisSDK/__init__.py create mode 100644 modules/TRPGNivisSDK/exception.py create mode 100644 modules/TRPGNivisSDK/execution.py create mode 100644 modules/TRPGNivisSDK/interpreter.py create mode 100644 modules/TRPGNivisSDK/lexer.py create mode 100644 modules/TRPGNivisSDK/mathmatics.py create mode 100644 modules/TRPGNivisSDK/parsers.py create mode 100644 modules/TRPGNivisSDK/type.py diff --git a/docs/source/api/GetPlayerCard.rst b/docs/source/api/GetPlayerCard.rst new file mode 100644 index 0000000..79fe5e4 --- /dev/null +++ b/docs/source/api/GetPlayerCard.rst @@ -0,0 +1,18 @@ +GetPlayerCard package +===================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + GetPlayerCard.version + +Module contents +--------------- + +.. automodule:: GetPlayerCard + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/GetPlayerCard.version.rst b/docs/source/api/GetPlayerCard.version.rst new file mode 100644 index 0000000..3117065 --- /dev/null +++ b/docs/source/api/GetPlayerCard.version.rst @@ -0,0 +1,7 @@ +GetPlayerCard.version module +============================ + +.. automodule:: GetPlayerCard.version + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRoll.cli.rst b/docs/source/api/HydroRoll.cli.rst new file mode 100644 index 0000000..3d940ae --- /dev/null +++ b/docs/source/api/HydroRoll.cli.rst @@ -0,0 +1,7 @@ +HydroRoll.cli module +==================== + +.. automodule:: HydroRoll.cli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRoll.config.rst b/docs/source/api/HydroRoll.config.rst new file mode 100644 index 0000000..99f81e8 --- /dev/null +++ b/docs/source/api/HydroRoll.config.rst @@ -0,0 +1,7 @@ +HydroRoll.config module +======================= + +.. automodule:: HydroRoll.config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRoll.exceptions.rst b/docs/source/api/HydroRoll.exceptions.rst new file mode 100644 index 0000000..51803a1 --- /dev/null +++ b/docs/source/api/HydroRoll.exceptions.rst @@ -0,0 +1,7 @@ +HydroRoll.exceptions module +=========================== + +.. automodule:: HydroRoll.exceptions + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRoll.models.rst b/docs/source/api/HydroRoll.models.rst new file mode 100644 index 0000000..5784541 --- /dev/null +++ b/docs/source/api/HydroRoll.models.rst @@ -0,0 +1,18 @@ +HydroRoll.models package +======================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + HydroRoll.models.utils + +Module contents +--------------- + +.. automodule:: HydroRoll.models + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/hydro_roll.exceptions.rst b/docs/source/api/HydroRoll.models.utils.rst similarity index 55% rename from docs/source/api/hydro_roll.exceptions.rst rename to docs/source/api/HydroRoll.models.utils.rst index bccb1d3..2400d09 100644 --- a/docs/source/api/hydro_roll.exceptions.rst +++ b/docs/source/api/HydroRoll.models.utils.rst @@ -1,7 +1,7 @@ -hydro\_roll.exceptions module +HydroRoll.models.utils module ============================= -.. automodule:: hydro_roll.exceptions +.. automodule:: HydroRoll.models.utils :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/hydro_roll.rst b/docs/source/api/HydroRoll.rst similarity index 50% rename from docs/source/api/hydro_roll.rst rename to docs/source/api/HydroRoll.rst index 182df2e..0b5009e 100644 --- a/docs/source/api/hydro_roll.rst +++ b/docs/source/api/HydroRoll.rst @@ -1,5 +1,5 @@ -hydro\_roll package -=================== +HydroRoll package +================= Subpackages ----------- @@ -7,7 +7,7 @@ Subpackages .. toctree:: :maxdepth: 4 - hydro_roll.models + HydroRoll.models Submodules ---------- @@ -15,16 +15,16 @@ Submodules .. toctree:: :maxdepth: 4 - hydro_roll.cli - hydro_roll.config - hydro_roll.exceptions - hydro_roll.typing - hydro_roll.utils + HydroRoll.cli + HydroRoll.config + HydroRoll.exceptions + HydroRoll.typing + HydroRoll.utils Module contents --------------- -.. automodule:: hydro_roll +.. automodule:: HydroRoll :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/HydroRoll.typing.rst b/docs/source/api/HydroRoll.typing.rst new file mode 100644 index 0000000..1357cc1 --- /dev/null +++ b/docs/source/api/HydroRoll.typing.rst @@ -0,0 +1,7 @@ +HydroRoll.typing module +======================= + +.. automodule:: HydroRoll.typing + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/hydro_roll.cli.rst b/docs/source/api/HydroRoll.utils.rst similarity index 58% rename from docs/source/api/hydro_roll.cli.rst rename to docs/source/api/HydroRoll.utils.rst index 93c24fe..2ebee05 100644 --- a/docs/source/api/hydro_roll.cli.rst +++ b/docs/source/api/HydroRoll.utils.rst @@ -1,7 +1,7 @@ -hydro\_roll.cli module +HydroRoll.utils module ====================== -.. automodule:: hydro_roll.cli +.. automodule:: HydroRoll.utils :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/hydro_roll.utils.rst b/docs/source/api/HydroRollCore.cli.rst similarity index 57% rename from docs/source/api/hydro_roll.utils.rst rename to docs/source/api/HydroRollCore.cli.rst index 3083df1..2a07983 100644 --- a/docs/source/api/hydro_roll.utils.rst +++ b/docs/source/api/HydroRollCore.cli.rst @@ -1,7 +1,7 @@ -hydro\_roll.utils module +HydroRollCore.cli module ======================== -.. automodule:: hydro_roll.utils +.. automodule:: HydroRollCore.cli :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/HydroRollCore.config.rst b/docs/source/api/HydroRollCore.config.rst new file mode 100644 index 0000000..6f7d0b7 --- /dev/null +++ b/docs/source/api/HydroRollCore.config.rst @@ -0,0 +1,7 @@ +HydroRollCore.config module +=========================== + +.. automodule:: HydroRollCore.config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.const.rst b/docs/source/api/HydroRollCore.const.rst new file mode 100644 index 0000000..9849c75 --- /dev/null +++ b/docs/source/api/HydroRollCore.const.rst @@ -0,0 +1,7 @@ +HydroRollCore.const module +========================== + +.. automodule:: HydroRollCore.const + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/hydro_roll.config.rst b/docs/source/api/HydroRollCore.core.rst similarity index 57% rename from docs/source/api/hydro_roll.config.rst rename to docs/source/api/HydroRollCore.core.rst index 026e76c..e70de3c 100644 --- a/docs/source/api/hydro_roll.config.rst +++ b/docs/source/api/HydroRollCore.core.rst @@ -1,7 +1,7 @@ -hydro\_roll.config module +HydroRollCore.core module ========================= -.. automodule:: hydro_roll.config +.. automodule:: HydroRollCore.core :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/HydroRollCore.dependencies.rst b/docs/source/api/HydroRollCore.dependencies.rst new file mode 100644 index 0000000..70fc3b2 --- /dev/null +++ b/docs/source/api/HydroRollCore.dependencies.rst @@ -0,0 +1,7 @@ +HydroRollCore.dependencies module +================================= + +.. automodule:: HydroRollCore.dependencies + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.dev.character.rst b/docs/source/api/HydroRollCore.dev.character.rst new file mode 100644 index 0000000..36e3114 --- /dev/null +++ b/docs/source/api/HydroRollCore.dev.character.rst @@ -0,0 +1,7 @@ +HydroRollCore.dev.character module +================================== + +.. automodule:: HydroRollCore.dev.character + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.dev.echo.rst b/docs/source/api/HydroRollCore.dev.echo.rst new file mode 100644 index 0000000..cb3785a --- /dev/null +++ b/docs/source/api/HydroRollCore.dev.echo.rst @@ -0,0 +1,7 @@ +HydroRollCore.dev.echo module +============================= + +.. automodule:: HydroRollCore.dev.echo + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.dev.grps.rst b/docs/source/api/HydroRollCore.dev.grps.rst new file mode 100644 index 0000000..e23a11c --- /dev/null +++ b/docs/source/api/HydroRollCore.dev.grps.rst @@ -0,0 +1,18 @@ +HydroRollCore.dev.grps package +============================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + HydroRollCore.dev.grps.v1 + +Module contents +--------------- + +.. automodule:: HydroRollCore.dev.grps + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.dev.grps.v1.rst b/docs/source/api/HydroRollCore.dev.grps.v1.rst new file mode 100644 index 0000000..32bec84 --- /dev/null +++ b/docs/source/api/HydroRollCore.dev.grps.v1.rst @@ -0,0 +1,7 @@ +HydroRollCore.dev.grps.v1 module +================================ + +.. automodule:: HydroRollCore.dev.grps.v1 + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.dev.rst b/docs/source/api/HydroRollCore.dev.rst new file mode 100644 index 0000000..aefe935 --- /dev/null +++ b/docs/source/api/HydroRollCore.dev.rst @@ -0,0 +1,27 @@ +HydroRollCore.dev package +========================= + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + HydroRollCore.dev.grps + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + HydroRollCore.dev.character + HydroRollCore.dev.echo + +Module contents +--------------- + +.. automodule:: HydroRollCore.dev + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.doc.rst b/docs/source/api/HydroRollCore.doc.rst new file mode 100644 index 0000000..0d0069f --- /dev/null +++ b/docs/source/api/HydroRollCore.doc.rst @@ -0,0 +1,10 @@ +HydroRollCore.doc package +========================= + +Module contents +--------------- + +.. automodule:: HydroRollCore.doc + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.event.rst b/docs/source/api/HydroRollCore.event.rst new file mode 100644 index 0000000..23c8f3b --- /dev/null +++ b/docs/source/api/HydroRollCore.event.rst @@ -0,0 +1,7 @@ +HydroRollCore.event module +========================== + +.. automodule:: HydroRollCore.event + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/hydro_roll.models.utils.rst b/docs/source/api/HydroRollCore.exceptions.rst similarity index 54% rename from docs/source/api/hydro_roll.models.utils.rst rename to docs/source/api/HydroRollCore.exceptions.rst index aacd2e9..57344c7 100644 --- a/docs/source/api/hydro_roll.models.utils.rst +++ b/docs/source/api/HydroRollCore.exceptions.rst @@ -1,7 +1,7 @@ -hydro\_roll.models.utils module +HydroRollCore.exceptions module =============================== -.. automodule:: hydro_roll.models.utils +.. automodule:: HydroRollCore.exceptions :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/HydroRollCore.feat.rst b/docs/source/api/HydroRollCore.feat.rst new file mode 100644 index 0000000..8b37858 --- /dev/null +++ b/docs/source/api/HydroRollCore.feat.rst @@ -0,0 +1,10 @@ +HydroRollCore.feat package +========================== + +Module contents +--------------- + +.. automodule:: HydroRollCore.feat + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.log.rst b/docs/source/api/HydroRollCore.log.rst new file mode 100644 index 0000000..2f068d6 --- /dev/null +++ b/docs/source/api/HydroRollCore.log.rst @@ -0,0 +1,7 @@ +HydroRollCore.log module +======================== + +.. automodule:: HydroRollCore.log + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.perf.rst b/docs/source/api/HydroRollCore.perf.rst new file mode 100644 index 0000000..d5ff9b8 --- /dev/null +++ b/docs/source/api/HydroRollCore.perf.rst @@ -0,0 +1,10 @@ +HydroRollCore.perf package +========================== + +Module contents +--------------- + +.. automodule:: HydroRollCore.perf + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.rst b/docs/source/api/HydroRollCore.rst new file mode 100644 index 0000000..a189068 --- /dev/null +++ b/docs/source/api/HydroRollCore.rst @@ -0,0 +1,39 @@ +HydroRollCore package +===================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + HydroRollCore.dev + HydroRollCore.doc + HydroRollCore.feat + HydroRollCore.perf + HydroRollCore.rule + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + HydroRollCore.cli + HydroRollCore.config + HydroRollCore.const + HydroRollCore.core + HydroRollCore.dependencies + HydroRollCore.event + HydroRollCore.exceptions + HydroRollCore.log + HydroRollCore.typing + HydroRollCore.utils + +Module contents +--------------- + +.. automodule:: HydroRollCore + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.rule.BaseRule.CharacterCard.rst b/docs/source/api/HydroRollCore.rule.BaseRule.CharacterCard.rst new file mode 100644 index 0000000..c4fcd35 --- /dev/null +++ b/docs/source/api/HydroRollCore.rule.BaseRule.CharacterCard.rst @@ -0,0 +1,7 @@ +HydroRollCore.rule.BaseRule.CharacterCard module +================================================ + +.. automodule:: HydroRollCore.rule.BaseRule.CharacterCard + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.rule.BaseRule.CustomRule.rst b/docs/source/api/HydroRollCore.rule.BaseRule.CustomRule.rst new file mode 100644 index 0000000..aa561df --- /dev/null +++ b/docs/source/api/HydroRollCore.rule.BaseRule.CustomRule.rst @@ -0,0 +1,7 @@ +HydroRollCore.rule.BaseRule.CustomRule module +============================================= + +.. automodule:: HydroRollCore.rule.BaseRule.CustomRule + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.rule.BaseRule.Wiki.rst b/docs/source/api/HydroRollCore.rule.BaseRule.Wiki.rst new file mode 100644 index 0000000..31d1c94 --- /dev/null +++ b/docs/source/api/HydroRollCore.rule.BaseRule.Wiki.rst @@ -0,0 +1,7 @@ +HydroRollCore.rule.BaseRule.Wiki module +======================================= + +.. automodule:: HydroRollCore.rule.BaseRule.Wiki + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.rule.BaseRule.rst b/docs/source/api/HydroRollCore.rule.BaseRule.rst new file mode 100644 index 0000000..1a297a4 --- /dev/null +++ b/docs/source/api/HydroRollCore.rule.BaseRule.rst @@ -0,0 +1,20 @@ +HydroRollCore.rule.BaseRule package +=================================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + HydroRollCore.rule.BaseRule.CharacterCard + HydroRollCore.rule.BaseRule.CustomRule + HydroRollCore.rule.BaseRule.Wiki + +Module contents +--------------- + +.. automodule:: HydroRollCore.rule.BaseRule + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/hydro_roll.models.rst b/docs/source/api/HydroRollCore.rule.rst similarity index 55% rename from docs/source/api/hydro_roll.models.rst rename to docs/source/api/HydroRollCore.rule.rst index 50723e0..7c3f194 100644 --- a/docs/source/api/hydro_roll.models.rst +++ b/docs/source/api/HydroRollCore.rule.rst @@ -1,18 +1,18 @@ -hydro\_roll.models package +HydroRollCore.rule package ========================== -Submodules ----------- +Subpackages +----------- .. toctree:: :maxdepth: 4 - hydro_roll.models.utils + HydroRollCore.rule.BaseRule Module contents --------------- -.. automodule:: hydro_roll.models +.. automodule:: HydroRollCore.rule :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/HydroRollCore.typing.rst b/docs/source/api/HydroRollCore.typing.rst new file mode 100644 index 0000000..a8a01b4 --- /dev/null +++ b/docs/source/api/HydroRollCore.typing.rst @@ -0,0 +1,7 @@ +HydroRollCore.typing module +=========================== + +.. automodule:: HydroRollCore.typing + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/HydroRollCore.utils.rst b/docs/source/api/HydroRollCore.utils.rst new file mode 100644 index 0000000..af30e69 --- /dev/null +++ b/docs/source/api/HydroRollCore.utils.rst @@ -0,0 +1,7 @@ +HydroRollCore.utils module +========================== + +.. automodule:: HydroRollCore.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/OneRoll.cli.rst b/docs/source/api/OneRoll.cli.rst new file mode 100644 index 0000000..f38c6b7 --- /dev/null +++ b/docs/source/api/OneRoll.cli.rst @@ -0,0 +1,7 @@ +OneRoll.cli module +================== + +.. automodule:: OneRoll.cli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/OneRoll.dice.rst b/docs/source/api/OneRoll.dice.rst new file mode 100644 index 0000000..29891ff --- /dev/null +++ b/docs/source/api/OneRoll.dice.rst @@ -0,0 +1,7 @@ +OneRoll.dice module +=================== + +.. automodule:: OneRoll.dice + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/OneRoll.diceast.rst b/docs/source/api/OneRoll.diceast.rst new file mode 100644 index 0000000..7f1f447 --- /dev/null +++ b/docs/source/api/OneRoll.diceast.rst @@ -0,0 +1,7 @@ +OneRoll.diceast module +====================== + +.. automodule:: OneRoll.diceast + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/OneRoll.errors.rst b/docs/source/api/OneRoll.errors.rst new file mode 100644 index 0000000..eceabb4 --- /dev/null +++ b/docs/source/api/OneRoll.errors.rst @@ -0,0 +1,7 @@ +OneRoll.errors module +===================== + +.. automodule:: OneRoll.errors + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/hydro_roll.typing.rst b/docs/source/api/OneRoll.expression.rst similarity index 57% rename from docs/source/api/hydro_roll.typing.rst rename to docs/source/api/OneRoll.expression.rst index 6638d5c..0ffc0c9 100644 --- a/docs/source/api/hydro_roll.typing.rst +++ b/docs/source/api/OneRoll.expression.rst @@ -1,7 +1,7 @@ -hydro\_roll.typing module +OneRoll.expression module ========================= -.. automodule:: hydro_roll.typing +.. automodule:: OneRoll.expression :members: :undoc-members: :show-inheritance: diff --git a/docs/source/api/OneRoll.rst b/docs/source/api/OneRoll.rst new file mode 100644 index 0000000..c36da13 --- /dev/null +++ b/docs/source/api/OneRoll.rst @@ -0,0 +1,24 @@ +OneRoll package +=============== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + OneRoll.cli + OneRoll.dice + OneRoll.diceast + OneRoll.errors + OneRoll.expression + OneRoll.stringifiers + OneRoll.utils + +Module contents +--------------- + +.. automodule:: OneRoll + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/OneRoll.stringifiers.rst b/docs/source/api/OneRoll.stringifiers.rst new file mode 100644 index 0000000..6bb1dd8 --- /dev/null +++ b/docs/source/api/OneRoll.stringifiers.rst @@ -0,0 +1,7 @@ +OneRoll.stringifiers module +=========================== + +.. automodule:: OneRoll.stringifiers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/OneRoll.utils.rst b/docs/source/api/OneRoll.utils.rst new file mode 100644 index 0000000..e595a13 --- /dev/null +++ b/docs/source/api/OneRoll.utils.rst @@ -0,0 +1,7 @@ +OneRoll.utils module +==================== + +.. automodule:: OneRoll.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.exception.rst b/docs/source/api/TRPGNivisSDK.exception.rst new file mode 100644 index 0000000..7299705 --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.exception.rst @@ -0,0 +1,7 @@ +TRPGNivisSDK.exception module +============================= + +.. automodule:: TRPGNivisSDK.exception + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.execution.rst b/docs/source/api/TRPGNivisSDK.execution.rst new file mode 100644 index 0000000..08468b5 --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.execution.rst @@ -0,0 +1,7 @@ +TRPGNivisSDK.execution module +============================= + +.. automodule:: TRPGNivisSDK.execution + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.interpreter.rst b/docs/source/api/TRPGNivisSDK.interpreter.rst new file mode 100644 index 0000000..0684762 --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.interpreter.rst @@ -0,0 +1,7 @@ +TRPGNivisSDK.interpreter module +=============================== + +.. automodule:: TRPGNivisSDK.interpreter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.lexer.rst b/docs/source/api/TRPGNivisSDK.lexer.rst new file mode 100644 index 0000000..2fe2ac7 --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.lexer.rst @@ -0,0 +1,7 @@ +TRPGNivisSDK.lexer module +========================= + +.. automodule:: TRPGNivisSDK.lexer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.mathmatics.rst b/docs/source/api/TRPGNivisSDK.mathmatics.rst new file mode 100644 index 0000000..33b8636 --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.mathmatics.rst @@ -0,0 +1,7 @@ +TRPGNivisSDK.mathmatics module +============================== + +.. automodule:: TRPGNivisSDK.mathmatics + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.parsers.rst b/docs/source/api/TRPGNivisSDK.parsers.rst new file mode 100644 index 0000000..e7e3d8e --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.parsers.rst @@ -0,0 +1,7 @@ +TRPGNivisSDK.parsers module +=========================== + +.. automodule:: TRPGNivisSDK.parsers + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.rst b/docs/source/api/TRPGNivisSDK.rst new file mode 100644 index 0000000..3e89228 --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.rst @@ -0,0 +1,24 @@ +TRPGNivisSDK package +==================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + TRPGNivisSDK.exception + TRPGNivisSDK.execution + TRPGNivisSDK.interpreter + TRPGNivisSDK.lexer + TRPGNivisSDK.mathmatics + TRPGNivisSDK.parsers + TRPGNivisSDK.type + +Module contents +--------------- + +.. automodule:: TRPGNivisSDK + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/TRPGNivisSDK.type.rst b/docs/source/api/TRPGNivisSDK.type.rst new file mode 100644 index 0000000..ccae3f3 --- /dev/null +++ b/docs/source/api/TRPGNivisSDK.type.rst @@ -0,0 +1,7 @@ +TRPGNivisSDK.type module +======================== + +.. automodule:: TRPGNivisSDK.type + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 0216263..2e79a4c 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -1,7 +1,11 @@ -hydro_roll -========== +modules +======= .. toctree:: :maxdepth: 4 - hydro_roll + GetPlayerCard + HydroRoll + HydroRollCore + OneRoll + TRPGNivisSDK diff --git a/modules/GetPlayerCard/__init__.py b/modules/GetPlayerCard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/GetPlayerCard/css/option_list.tcss b/modules/GetPlayerCard/css/option_list.tcss new file mode 100644 index 0000000..a9ad747 --- /dev/null +++ b/modules/GetPlayerCard/css/option_list.tcss @@ -0,0 +1,8 @@ +Screen { + align: center middle; +} + +OptionList { + width: 70%; + height: 80%; +} \ No newline at end of file diff --git a/modules/GetPlayerCard/templates/tests.template b/modules/GetPlayerCard/templates/tests.template new file mode 100644 index 0000000..e599968 --- /dev/null +++ b/modules/GetPlayerCard/templates/tests.template @@ -0,0 +1 @@ +hi, {{ user }} \ No newline at end of file diff --git a/modules/GetPlayerCard/version.py b/modules/GetPlayerCard/version.py new file mode 100644 index 0000000..4c2a3bc --- /dev/null +++ b/modules/GetPlayerCard/version.py @@ -0,0 +1,3 @@ +from packaging.version import Version + +__version__ = Version("0.1.0") diff --git a/modules/HydroRoll/__init__.py b/modules/HydroRoll/__init__.py new file mode 100644 index 0000000..006c6d4 --- /dev/null +++ b/modules/HydroRoll/__init__.py @@ -0,0 +1,80 @@ +"""中间件""" + +from ast import literal_eval +import os +from os.path import dirname, join, abspath +from iamai import ConfigModel, Plugin +from iamai.log import logger +from .config import Directory +from .models.utils import * +import joblib + +try: + from .hydro_roll import sum_as_string +except ImportError: + ... + +BASE_DIR = Directory(_path=dirname(abspath("__file__"))) +HYDRO_DIR = dirname(abspath(__file__)) + + +def _init_directory(_prefix: str = ""): + """初始化水系目录""" + for _ in BASE_DIR.get_dice_dir_list(_prefix): + if not os.path.exists(_): + os.makedirs(_) + + +def _load_models(): + models = {} + models["hola"] = joblib.load(join(HYDRO_DIR, "models", "hola.pkl")) + return models + + +def load_model(model): + logger.info("loading models...") + return _load_models()[model] + + +def init_directory(_prefix: str = "HydroRoll"): + _init_directory(_prefix=_prefix) + + +class HydroRoll(Plugin): + """中间件""" + + class Config(ConfigModel): + __config_name__ = "HydroRoll" + + priority = 0 + + # TODO: infini should be able to handle all signals and tokens from Psi. + logger.info(f"Loading infini... with {sum_as_string(2,3)}") + + async def handle(self) -> None: + """ + @TODO: infini should be able to handle all signals and tokens from Psi. + @BODY: infini actives the rule-packages. + """ + + if self.event.message.get_plain_text() == ".core": + await self.event.reply("infini is running.") + elif self.event.message.startswith(".test"): + try: + result = literal_eval(self.event.message.get_plain_text()[5:]) + await self.event.reply(result) + except Exception as error: + await self.event.reply(f"{error!r}") + + async def rule(self) -> bool: + """ + @TODO: Psi should be able to handle all message first. + @BODY: lexer module will return a list of tokens, parser module will parse the tokens into a tree, and executor module will execute the tokens with a stack with a bool return value. + """ + logger.info("loading psi...") + if not self.bot.global_state.get("HydroRoll.dir"): + hola = load_model("hola") + + init_directory() + self.bot.global_state["HydroRoll.dir"] = True + return self.event.adapter.name in ["cqhttp", "kook", "console", "mirai"] diff --git a/modules/HydroRoll/cli.py b/modules/HydroRoll/cli.py new file mode 100644 index 0000000..2588ad8 --- /dev/null +++ b/modules/HydroRoll/cli.py @@ -0,0 +1,186 @@ +import argparse +import os +import aiohttp +import asyncio +import json +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from .typing import * + + +class Atien(object): + parser = argparse.ArgumentParser(description="水系终端脚手架") + + def __init__(self): + self.parser.add_argument( + "-i", + "--install", + dest="command", + help="安装规则包、插件与模型", + action="store_const", + const="install_package", + ) + self.parser.add_argument( + "-T", + "--template", + dest="command", + help="选择模板快速创建Bot实例", + action="store_const", + const="build_template", + ) + self.parser.add_argument( + "-S", + "--search", + dest="command", + help="在指定镜像源查找规则包、插件与模型", + action="store_const", + const="search_package", + ) + self.parser.add_argument( + "-c", + "--config", + dest="command", + help="配置管理", + action="store_const", + const="config", + ) + self.args = self.parser.parse_args() + + def get_args(self): + return self.args + + def get_help(self): + return self.parser.format_help() + + async def install_packages(self): + package_name = input("请输入要安装的包名:") + url = f"https://pypi.org/pypi/{package_name}/json" + + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data = await response.json() + await self._extract_package(data, package_name) + else: + print(f"找不到包:{package_name}") + + async def _extract_package(self, data, package_name): + latest_version = data["info"]["version"] + download_url = data["releases"][latest_version][0]["url"] + + plugins_dir = "plugins" + if not os.path.exists(plugins_dir): + os.mkdir(plugins_dir) + + file_name = download_url.split("/")[-1] + file_path = os.path.join(plugins_dir, file_name) + + async with aiohttp.ClientSession() as session: + async with session.get(download_url) as response: + if response.status == 200: + with open(file_path, "wb") as file: + file.write(await response.read()) + print(f"成功安装包:{package_name}") + else: + print(f"下载包时出错:{package_name}") + + def build_template(self): + template = input( + "请选择应用模板(输入数字):\n" + "1. 创建轻量应用\n" + "2. 创建标准应用\n" + "3. 创建开发应用\n" + ) + + if template == "1": + print("选择了轻量应用模板") + elif template == "2": + print("选择了标准应用模板") + elif template == "3": + print("选择了开发应用模板") + else: + print("无效的模板选择") + + async def search_package(self): + search_term = input("请输入要搜索的包名关键字:") + url = f"https://pypi.org/search/?q={search_term}" + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + data: dict = response.json() # type: ignore[dict] + packages = data.get("results", []) + + for package in packages: + name = package["name"] + topics = package.get("topics", []) + + if ( + search_term.lower() in name.lower() + and "HydroRoll" in topics + ): + print(f"包名:{name}") + else: + print("搜索失败") + + def config(self): + config_dir = os.path.expanduser("~/.hydroroll") + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + config_file = os.path.join(config_dir, "config.json") + + subcommand = input("请输入子命令(add/delete):") + + if subcommand == "add": + key = input("请输入要添加的键名:") + value = input("请输入要添加的键值:") + + with open(config_file, "r+") as file: + try: + config_data = json.load(file) + except json.JSONDecodeError: + config_data = {} + + config_data[key] = value + self._extracted_from_config_21(file, config_data) + print(f"成功添加配置项:{key}={value}") + + elif subcommand == "delete": + key = input("请输入要删除的键名:") + + with open(config_file, "r+") as file: + try: + config_data = json.load(file) + except json.JSONDecodeError: + config_data = {} + + if key in config_data: + del config_data[key] + self._extracted_from_config_21(file, config_data) + print(f"成功删除配置项:{key}") + else: + print(f"配置项不存在:{key}") + + else: + print("无效的子命令选择") + + # TODO Rename this here and in `config` + def _extracted_from_config_21(self, file, config_data): + file.seek(0) + json.dump(config_data, file, indent=4) + file.truncate() + + +cli = Cli() + +if cli.get_args().command == "install_package": + asyncio.run(cli.install_packages()) +elif cli.get_args().command == "build_template": + cli.build_template() +elif cli.get_args().command == "search_package": + asyncio.run(cli.search_package()) +elif cli.get_args().command == "config": + cli.config() +else: + print(cli.get_help()) diff --git a/modules/HydroRoll/config.py b/modules/HydroRoll/config.py new file mode 100644 index 0000000..a1b1d8a --- /dev/null +++ b/modules/HydroRoll/config.py @@ -0,0 +1,96 @@ +import argparse +import sys +import platform +from importlib.metadata import version +import os +from typing import Set, Optional +from iamai import ConfigModel + +# 创建全局 ArgumentParser 对象 +global_parser = argparse.ArgumentParser(description="HydroRoll[水系] 全局命令参数") + + +class BasePluginConfig(ConfigModel): + __config_name__ = "" + handle_all_message: bool = True + """是否处理所有类型的消息,此配置为 True 时会覆盖 handle_friend_message 和 handle_group_message。""" + handle_friend_message: bool = True + """是否处理好友消息。""" + handle_group_message: bool = True + """是否处理群消息。""" + accept_group: Optional[Set[int]] = None + """处理消息的群号,仅当 handle_group_message 为 True 时生效,留空表示处理所有群。""" + message_str: str = "*{user_name} {message}" + """最终发送消息的格式。""" + + +class RegexPluginConfig(BasePluginConfig): + pass + + +class CommandPluginConfig(RegexPluginConfig): + command_prefix: Set[str] = {":", "你妈", "👅", "约瑟夫妥斯妥耶夫斯基戴安那只鸡🐔"} + """命令前缀。""" + command: Set[str] = {} + """命令文本。""" + ignore_case: bool = True + """忽略大小写。""" + + +# 定义全局配置类 +class GlobalConfig(CommandPluginConfig): + _name = "HydroRoll[水系]" + _version = "0.1.0" + _svn = "2" + _author = "简律纯" + _iamai_version = version("iamai") + _python_ver = sys.version + _python_ver_raw = ".".join(map(str, platform.python_version_tuple()[:3])) + + # 定义系统组件 + class HydroSystem: + def __init__(self): + self.parser = argparse.ArgumentParser( + description="HydroRoll[水系].system 系统命令参数" + ) + self.subparsers = self.parser.add_subparsers() + self.status_parser = self.subparsers.add_parser( + "status", aliases=["stat", "state"], help="系统状态" + ) + self.reload_parser = self.subparsers.add_parser( + "reload", aliases=["rld"], help="重新加载系统" + ) + self.restart_parser = self.subparsers.add_parser( + "restart", aliases=["rst"], help="重启系统" + ) + self.collect_parser = self.subparsers.add_parser( + "collect", aliases=["gc"], help="释放 python 内存" + ) + self.help = "\n".join( + self.parser.format_help() + .replace("\r\n", "\n") + .replace("\r", "") + .split("\n")[2:-3] + ) + + class HydroBot: + def __init__(self) -> None: + self.parser = argparse.ArgumentParser(description="Bot命令") + + +class Directory(object): + def __init__(self, _path: str) -> None: + self.current_path = _path + + def get_dice_dir_list(self, _prefix: str) -> list: + return [ + os.path.join(self.current_path, f"{_prefix}", *dirs) + for dirs in [ + ["config"], + ["data"], + ["rules"], + ["scripts"], + ["web", "frontend"], + ["web", "backend"], + ] + ] diff --git a/modules/HydroRoll/exceptions.py b/modules/HydroRoll/exceptions.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRoll/models/__init__.py b/modules/HydroRoll/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRoll/models/hola.pkl b/modules/HydroRoll/models/hola.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b9940faec08f07a044f939739e3f9caba7e43958 GIT binary patch literal 209 zcmZo*nR<)?0&1sd^zbK@X6BS+=EWD66lLb67f*==3UXy+>L@(hu;uyY-BWsaK^%|D zl%o8g{G1#RKQ$*OA0aOQC(LjC4w*kgrd|Q;k3-W ll=$4liumHp+{~QBqRf)YDLowVKp~)m max_similarity: + max_similarity = similarity + max_string = string + + return max_string, max_similarity diff --git a/modules/HydroRoll/typing.py b/modules/HydroRoll/typing.py new file mode 100644 index 0000000..da83c36 --- /dev/null +++ b/modules/HydroRoll/typing.py @@ -0,0 +1,34 @@ +"""HydroRoll 类型提示支持。 + +此模块定义了部分 HydroRoll 使用的类型。 +""" + +from typing import TYPE_CHECKING, TypeVar, Callable, NoReturn, Awaitable + +from iamai.message import T_MS, T_Message, T_MessageSegment + +if TYPE_CHECKING: + from iamai.bot import Bot # noqa + from iamai.event import Event # noqa + from iamai.plugin import Plugin # noqa + from iamai.config import ConfigModel # noqa + +__all__ = [ + "T_State", + "T_Event", + "T_Plugin", + "T_Config", + "T_Message", + "T_MessageSegment", + "T_MS", + "T_BotHook", + "T_EventHook", +] + +T_State = TypeVar("T_State") +T_Event = TypeVar("T_Event", bound="Event") +T_Plugin = TypeVar("T_Plugin", bound="Plugin") +T_Config = TypeVar("T_Config", bound="ConfigModel") + +T_BotHook = Callable[["Bot"], Awaitable[NoReturn]] +T_EventHook = Callable[[T_Event], Awaitable[NoReturn]] diff --git a/modules/HydroRoll/utils.py b/modules/HydroRoll/utils.py new file mode 100644 index 0000000..453e691 --- /dev/null +++ b/modules/HydroRoll/utils.py @@ -0,0 +1,162 @@ +import re +import time +import random +from abc import ABC, abstractmethod +from typing import Type, Union, Generic, TypeVar +from iamai import Plugin +from iamai.typing import T_State +from iamai.adapter.cqhttp.event import GroupMessageEvent, PrivateMessageEvent + +from .config import BasePluginConfig, RegexPluginConfig, CommandPluginConfig + +T_Config = TypeVar("T_Config", bound=BasePluginConfig) +T_RegexPluginConfig = TypeVar("T_RegexPluginConfig", bound=RegexPluginConfig) +T_CommandPluginConfig = TypeVar("T_CommandPluginConfig", bound=CommandPluginConfig) + + +class BasePlugin( + Plugin[Union[PrivateMessageEvent, GroupMessageEvent], T_State, T_Config], + ABC, + Generic[T_State, T_Config], +): + Config: Type[T_Config] = BasePluginConfig + + def format_str(self, format_str: str, message_str: str = "") -> str: + return format_str.format( + message=message_str, + user_name=self.event.sender.nickname, + user_id=self.event.sender.user_id, + ) + + async def rule(self) -> bool: + is_bot_off = False + + if self.event.adapter.name != "cqhttp": + return False + if self.event.type != "message": + return False + match_str = self.event.message.get_plain_text() + if is_bot_off: + if self.event.message.startswith(f"[CQ:at,qq={self.event.self_id}]"): + match_str = re.sub( + rf"^\[CQ:at,qq={self.event.self_id}\]", "", match_str + ) + elif self.event.message.startswith(f"[CQ:at,qq={self.event.self_tiny_id}]"): + match_str = re.sub( + rf"^\[CQ:at,qq={self.event.self_tiny_id}\]", "", match_str + ) + else: + return False + if self.config.handle_all_message: + return self.str_match(match_str) + elif self.config.handle_friend_message: + if self.event.message_type == "private": + return self.str_match(match_str) + elif self.config.handle_group_message: + if self.event.message_type == "group": + if ( + self.config.accept_group is None + or self.event.group_id in self.config.accept_group + ): + return self.str_match(match_str) + elif self.config.handle_group_message: + if self.event.message_type == "guild": + return self.str_match(match_str) + return False + + @abstractmethod + def str_match(self, msg_str: str) -> bool: + raise NotImplemented + + +class RegexPluginBase(BasePlugin[T_State, T_RegexPluginConfig], ABC): + msg_match: re.Match + re_pattern: re.Pattern + Config: Type[T_RegexPluginConfig] = RegexPluginConfig + + def str_match(self, msg_str: str) -> bool: + msg_str = msg_str.strip() + self.msg_match = self.re_pattern.fullmatch(msg_str) + return bool(self.msg_match) + + +class CommandPluginBase(RegexPluginBase[T_State, T_CommandPluginConfig], ABC): + command_match: re.Match + command_re_pattern: re.Pattern + Config: Type[T_CommandPluginConfig] = CommandPluginConfig + + def str_match(self, msg_str: str) -> bool: + if not hasattr(self, "command_re_pattern"): + self.command_re_pattern = re.compile( + f'({"|".join(self.config.command_prefix)})' + f'({"|".join(self.config.command)})' + r"\s*(?P.*)", + flags=re.I if self.config.ignore_case else 0, + ) + msg_str = msg_str.strip() + self.command_match = self.command_re_pattern.fullmatch(msg_str) + if not self.command_match: + return False + self.msg_match = self.re_pattern.fullmatch( + self.command_match.group("command_args") + ) + return bool(self.msg_match) + + +class PseudoRandomGenerator: + """线性同余法随机数生成器""" + + def __init__(self, seed): + self.seed = seed + + def generate(self): + while True: + self.seed = (self.seed * 1103515245 + 12345) % (2**31) + yield self.seed + + +class HydroDice: + """水系掷骰组件 + + 一些 API 相关的工具函数 + + """ + + def __init__(self, seed): + self.generator = PseudoRandomGenerator(seed) + + def roll_dice( + self, + _counts: int | str, + _sides: int | str, + is_reversed: bool = False, + streamline: bool = False, + threshold: int | str = 5, + ) -> str: + """普通掷骰 + Args: + _counts (int | str): 掷骰个数. + _sides (int | str): 每个骰子的面数. + is_reversed (bool, optional): 倒序输出. Defaults to False. + streamline (bool, optional): 忽略过程. Defaults to False. + threshold (int | str, optional): streamline 的阈值. Defaults to 5. + + Returns: + str: 表达式结果. + """ + rolls = [] + for _ in range(int(_counts)): + roll = next(self.generator.generate()) % _sides + 1 + rolls.append(roll) + total = sum(rolls) + + if streamline: + return str(total) + else: + if len(rolls) > int(threshold): + return str(total) + rolls_str = " + ".join(str(r) for r in rolls) + result_str = ( + f"{total} = {rolls_str}" if is_reversed else f"{rolls_str} = {total}" + ) + return result_str diff --git a/modules/HydroRollCore/LibCore.pyi b/modules/HydroRollCore/LibCore.pyi new file mode 100644 index 0000000..aa88747 --- /dev/null +++ b/modules/HydroRollCore/LibCore.pyi @@ -0,0 +1,5 @@ +class LibCore(object): + """Core library for hydro roll""" + + def __init__(self, name: str = ""): ... + def process_rule_pack(self, rule_pack: str) -> str: ... diff --git a/modules/HydroRollCore/__init__.py b/modules/HydroRollCore/__init__.py new file mode 100644 index 0000000..b607349 --- /dev/null +++ b/modules/HydroRollCore/__init__.py @@ -0,0 +1,13 @@ +from .LibCore import * # noqa: F403 + +from . import rule # noqa: F401 +from . import core # noqa: F401 +from . import log # noqa: F401 +from . import exceptions # noqa: F401 +from . import config # noqa: F401 +from . import dependencies # noqa: F401 +from . import event # noqa: F401 +from . import perf # noqa: F401 +from . import feat # noqa: F401 +from . import doc # noqa: F401 +from . import dev # noqa: F401 diff --git a/modules/HydroRollCore/cli.py b/modules/HydroRollCore/cli.py new file mode 100644 index 0000000..55758bc --- /dev/null +++ b/modules/HydroRollCore/cli.py @@ -0,0 +1,46 @@ +import argparse + + +class Cli(object): + parser = argparse.ArgumentParser(description="水系核心终端") + + def __init__(self): + self.parser.add_argument( + "-i", + "--install", + dest="command", + help="安装规则包", + action="store_const", + const="install_package", + ) + self.parser.add_argument( + "-T", + "--template", + dest="command", + help="选择模板快速创建规则包实例", + action="store_const", + const="build_template", + ) + self.parser.add_argument( + "-S", + "--search", + dest="command", + help="在指定镜像源查找规则包", + action="store_const", + const="search_package", + ) + self.parser.add_argument( + "-c", + "--config", + dest="command", + help="配置管理", + action="store_const", + const="config", + ) + self.args = self.parser.parse_args() + + def get_args(self): + return self.args + + def get_help(self): + return self.parser.format_help() diff --git a/modules/HydroRollCore/config.py b/modules/HydroRollCore/config.py new file mode 100644 index 0000000..b6458b2 --- /dev/null +++ b/modules/HydroRollCore/config.py @@ -0,0 +1,29 @@ +from typing import Set, Union + +from pydantic import BaseModel, ConfigDict, DirectoryPath, Field + + +class ConfigModel(BaseModel): + model_config = ConfigDict(extra="allow") + + __config_name__: str = "" + + +class LogConfig(ConfigModel): + level: Union[str, int] = "DEBUG" + verbose_exception: bool = False + + +class CoreConfig(ConfigModel): + rules: Set[str] = Field(default_factory=set) + rule_dirs: Set[DirectoryPath] = Field(default_factory=set) + log: LogConfig = LogConfig() + + +class RuleConfig(ConfigModel): + """rule configuration.""" + + +class MainConfig(ConfigModel): + core: CoreConfig = CoreConfig() + rule: RuleConfig = RuleConfig() diff --git a/modules/HydroRollCore/const.py b/modules/HydroRollCore/const.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRollCore/core.py b/modules/HydroRollCore/core.py new file mode 100644 index 0000000..86f8927 --- /dev/null +++ b/modules/HydroRollCore/core.py @@ -0,0 +1,638 @@ +import asyncio +import json +import pkgutil +import signal +import sys +import threading +import time # noqa: F401 +from collections import defaultdict +from contextlib import AsyncExitStack +from itertools import chain +from pathlib import Path +from typing import ( + Any, + Awaitable, # noqa: F401 + Callable, # noqa: F401 + Dict, + List, + Optional, + Set, # noqa: F401 + Tuple, + Type, + Union, + overload, # noqa: F401 +) + +from pydantic import ValidationError, create_model + +from .config import ConfigModel, MainConfig, RuleConfig +from .dependencies import solve_dependencies +from .log import logger +from .rule import Rule, RuleLoadType +from .event import Event +from .typing import CoreHook, EventHook, EventT, RuleHook # noqa: F401 +from .utils import ( + ModulePathFinder, + get_classes_from_module_name, + is_config_class, + samefile, + wrap_get_func, # noqa: F401 +) +from .exceptions import StopException, SkipException, GetEventTimeout, LoadModuleError # noqa: F401 + + +if sys.version_info >= (3, 11): # pragma: no cover + import tomllib +else: # pragma: no cover + import tomli as tomllib + +HANDLED_SIGNALS = ( + signal.SIGINT, # Unix signal 2. Sent by Ctrl+C. + signal.SIGTERM, # Unix signal 15. Sent by `kill `. +) + + +class Core: + config: MainConfig + _current_event: Optional[Event[Any]] + _module_path_finder: ModulePathFinder + _hot_reload: bool + # pyright: ignore[reportUninitializedInstanceVariable] + should_exit: asyncio.Event + _restart_flag: bool # Restart flag + _extend_rules: List[Union[Type[Rule[Any, Any, Any]], str, Path]] + _extend_rule_dirs: List[Path] + rules_priority_dict: Dict[int, List[Type[Rule[Any, Any, Any]]]] + _config_file: Optional[str] # Configuration file + _config_dict: Optional[Dict[str, Any]] # Configuration dictionary + + _condition: ( + asyncio.Condition + ) # Condition used to handle get # pyright: ignore[reportUninitializedInstanceVariable] + _rule_tasks: Set[ + "asyncio.Task[None]" + ] # Adapter task collection, used to hold references to adapter tasks + _handle_event_tasks: Set[ + "asyncio.Task[None]" + ] # Event handling task, used to keep a reference to the adapter task + + def __init__( + self, + *, + config_file: Optional[str] = "config.toml", + config_dict: Optional[Dict[str, Any]] = None, + hot_reload: bool = False, + ) -> None: + self.config = MainConfig() + self._current_event = None + self._config_file = config_file + self._config_dict = config_dict + self._hot_reload = hot_reload + self._restart_flag = False + self._module_path_finder = ModulePathFinder() + self.rules_priority_dict = defaultdict(list) + self._raw_config_dict = {} + self._rule_tasks = set() + self._handle_event_tasks = set() + + self._extend_rules = [] + self._extend_rule_dirs = [] + self._core_run_hooks = [] + self._core_exit_hooks = [] + self._rule_enable_hooks = [] + self._rule_run_hooks = [] + self._rule_disable_hooks = [] + self._event_preprocessor_hooks = [] + self._event_postprocessor_hooks = [] + + sys.meta_path.insert(0, self._module_path_finder) + + @property + def rules(self) -> List[Type[Rule[Any, Any, Any]]]: + return list(chain(*self.rules_priority_dict.values())) + + def run(self) -> None: + self._restart_flag = True + while self._restart_flag: + self._restart_flag = False + asyncio.run(self._run()) + if self._restart_flag: + self._load_rules_from_dirs(*self._extend_rule_dirs) + self._load_rules(*self._extend_rules) + + def restart(self) -> None: + logger.info("Restarting...") + self._restart_flag = True + self.should_exit.set() + + async def _run(self) -> None: + self.should_exit = asyncio.Event() + self._condition = asyncio.Condition() + + # Monitor and intercept system exit signals to complete some aftermath work before closing the program + if threading.current_thread() is threading.main_thread(): # pragma: no cover + # Signals can only be processed in the main thread + try: + loop = asyncio.get_running_loop() + for sig in HANDLED_SIGNALS: + loop.add_signal_handler(sig, self._handle_exit) + except NotImplementedError: + # add_signal_handler is only available under Unix, below for Windows + for sig in HANDLED_SIGNALS: + signal.signal(sig, self._handle_exit) + + # Load configuration file + self._reload_config_dict() + + self._load_rules_from_dirs(*self.config.core.rule_dirs) + self._load_rules(*self.config.core.rules) + self._update_config() + + logger.info("Running...") + + hot_reload_task = None + if self._hot_reload: # pragma: no cover + hot_reload_task = asyncio.create_task(self._run_hot_reload()) + + for core_run_hook_func in self._core_run_hooks: + await core_run_hook_func(self) + + try: + # TODO(简律纯): builtin rule enable hook function in every rules packages. + # for _rule in self.rules: + # for rule_enable_hook_func in self._rule_enable_hooks: + # await rule_enable_hook_func(_rule) + # try: + # await _rule.enable() + # except Exception as e: + # self.error_or_exception( + # f"Enable rule {_rule!r} failed:", e) + # TODO(简律纯): builtin rule run hook function in every rules packages. + # for _rule in self.rules: + # for rule_run_hook_func in self._rule_run_hooks: + # await rule_run_hook_func(_rule) + # _rule_task = asyncio.create_task(_rule.safe_run()) + # self._rule_tasks.add(_rule_task) + # _rule_task.add_done_callback(self._rule_tasks.discard) + + await self.should_exit.wait() + + if hot_reload_task is not None: # pragma: no cover + await hot_reload_task + finally: + # TODO(简律纯): builtin rule disable hook function in every rules packages. + # for _rule in self.rules: + # for rule_disable_hook_func in self._rule_disable_hooks: + # await rule_disable_hook_func(_rule) + # await _rule.disable() + + while self._rule_tasks: + await asyncio.sleep(0) + + for core_exit_hook_func in self._core_exit_hooks: + await core_exit_hook_func(self) + + self.rules.clear() + self.rules_priority_dict.clear() + self._module_path_finder.path.clear() + + def _remove_rule_by_path( + self, file: Path + ) -> List[Type[Rule[Any, Any, Any]]]: # pragma: no cover + removed_rules: List[Type[Rule[Any, Any, Any]]] = [] + for rules in self.rules_priority_dict.values(): + _removed_rules = list( + filter( + lambda x: x.__rule_load_type__ != RuleLoadType.CLASS + and x.__rule_file_path__ is not None + and samefile(x.__rule_file_path__, file), + rules, + ) + ) + removed_rules.extend(_removed_rules) + for rule_ in _removed_rules: + rules.remove(rule_) + logger.info( + "Succeeded to remove rule " f'"{rule_.__name__}" from file "{file}"' + ) + return removed_rules + + async def _run_hot_reload(self) -> None: # pragma: no cover + """Hot reload.""" + try: + from watchfiles import Change, awatch + except ImportError: + logger.warning( + 'Hot reload needs to install "watchfiles", try "pip install watchfiles"' + ) + return + + logger.info("Hot reload is working!") + async for changes in awatch( + *( + x.resolve() + for x in set(self._extend_rule_dirs) + .union(self.config.core.rule_dirs) + .union( + {Path(self._config_file)} + if self._config_dict is None and self._config_file is not None + else set() + ) + ), + stop_event=self.should_exit, + ): + # Processed in the order of Change.deleted, Change.modified, Change.added + # To ensure that when renaming occurs, deletions are processed first and then additions are processed + for change_type, file_ in sorted(changes, key=lambda x: x[0], reverse=True): + file = Path(file_) + # Change configuration file + if ( + self._config_file is not None + and samefile(self._config_file, file) + and change_type == change_type.modified + ): + logger.info(f'Reload config file "{self._config_file}"') + old_config = self.config + self._reload_config_dict() + if ( + self.config.core != old_config.core + or self.config.rule != old_config.rule + ): + self.restart() + continue + + # Change rule folder + if change_type == Change.deleted: + # Special handling for deletion operations + if file.suffix != ".py": + file = file / "__init__.py" + else: + if file.is_dir() and (file / "__init__.py").is_file(): + # When a new directory is added and this directory contains the ``__init__.py`` file + # It means that what happens at this time is that a Python package is added, and the ``__init__.py`` file of this package is deemed to be added + file = file / "__init__.py" + if not (file.is_file() and file.suffix == ".py"): + continue + + if change_type == Change.added: + logger.info(f"Hot reload: Added file: {file}") + self._load_rules( + Path(file), rule_load_type=RuleLoadType.DIR, reload=True + ) + self._update_config() + continue + if change_type == Change.deleted: + logger.info(f"Hot reload: Deleted file: {file}") + self._remove_rule_by_path(file) + self._update_config() + elif change_type == Change.modified: + logger.info(f"Hot reload: Modified file: {file}") + self._remove_rule_by_path(file) + self._load_rules( + Path(file), rule_load_type=RuleLoadType.DIR, reload=True + ) + self._update_config() + + def _update_config(self) -> None: + def update_config( + source: List[Type[Rule[Any, Any, Any]]], + name: str, + base: Type[ConfigModel], + ) -> Tuple[Type[ConfigModel], ConfigModel]: + config_update_dict: Dict[str, Any] = {} + for i in source: + config_class = getattr(i, "Config", None) + if is_config_class(config_class): + default_value: Any + try: + default_value = config_class() + except ValidationError: + default_value = ... + config_update_dict[config_class.__config_name__] = ( + config_class, + default_value, + ) + config_model = create_model( + name, **config_update_dict, __base__=base) + return config_model, config_model() + + self.config = create_model( + "Config", + rule=update_config(self.rules, "RuleConfig", RuleConfig), + __base__=MainConfig, + )(**self._raw_config_dict) + # Update the level of logging + logger.remove() + logger.add(sys.stderr, level=self.config.core.log.level) + + def _reload_config_dict(self) -> None: + """Reload the configuration file.""" + self._raw_config_dict = {} + + if self._config_dict is not None: + self._raw_config_dict = self._config_dict + elif self._config_file is not None: + try: + with Path(self._config_file).open("rb") as f: + if self._config_file.endswith(".json"): + self._raw_config_dict = json.load(f) + elif self._config_file.endswith(".toml"): + self._raw_config_dict = tomllib.load(f) + else: + self.error_or_exception( + "Read config file failed:", + OSError("Unable to determine config file type"), + ) + except OSError as e: + self.error_or_exception("Can not open config file:", e) + except (ValueError, json.JSONDecodeError, tomllib.TOMLDecodeError) as e: + self.error_or_exception("Read config file failed:", e) + + try: + self.config = MainConfig(**self._raw_config_dict) + except ValidationError as e: + self.config = MainConfig() + self.error_or_exception("Config dict parse error:", e) + self._update_config() + + def reload_rules(self) -> None: + """Manually reload all rules.""" + self.rules_priority_dict.clear() + self._load_rules(*self.config.core.rules) + self._load_rules_from_dirs(*self.config.core.rule_dirs) + self._load_rules(*self._extend_rules) + self._load_rules_from_dirs(*self._extend_rule_dirs) + self._update_config() + + def _handle_exit(self, *_args: Any) -> None: # pragma: no cover + """When the robot receives the exit signal, it will handle it according to the situation.""" + logger.info("Stopping...") + if self.should_exit.is_set(): + logger.warning("Force Exit...") + sys.exit() + else: + self.should_exit.set() + + async def handle_event( + self, + current_event: Event[Any], + *, + handle_get: bool = True, + show_log: bool = True, + ) -> None: + if show_log: + logger.info( + f"Rule {current_event.rule.name} received: {current_event!r}") + + if handle_get: + _handle_event_task = asyncio.create_task(self._handle_event()) + self._handle_event_tasks.add(_handle_event_task) + _handle_event_task.add_done_callback( + self._handle_event_tasks.discard) + await asyncio.sleep(0) + async with self._condition: + self._current_event = current_event + self._condition.notify_all() + else: + _handle_event_task = asyncio.create_task( + self._handle_event(current_event)) + self._handle_event_tasks.add(_handle_event_task) + _handle_event_task.add_done_callback( + self._handle_event_tasks.discard) + + async def _handle_event(self, current_event: Optional[Event[Any]] = None) -> None: + if current_event is None: + async with self._condition: + await self._condition.wait() + assert self._current_event is not None + current_event = self._current_event + if current_event.__handled__: + return + + for _hook_func in self._event_preprocessor_hooks: + await _hook_func(current_event) + + for rule_priority in sorted(self.rules_priority_dict.keys()): + logger.debug( + f"Checking for matching rules with priority {rule_priority!r}" + ) + stop = False + for rule in self.rules_priority_dict[rule_priority]: + try: + async with AsyncExitStack() as stack: + _rule = await solve_dependencies( + rule, + use_cache=True, + stack=stack, + dependency_cache={ + Core: self, + Event: current_event, + }, + ) + if _rule.name not in self.rule_state: + rule_state = _rule.__init_state__() + if rule_state is not None: + self.rule_state[_rule.name] = rule_state + # TODO(简律纯): Refactor event handle process with General Rules Package Standard + if await _rule.rule(): + logger.info(f"Event will be handled by {_rule!r}") + try: + await _rule.handle() + finally: + if _rule.block: + stop = True + except SkipException: + # The plug-in requires that it skips itself and continues the current event propagation + continue + except StopException: + # rule requires stopping current event propagation + stop = True + except Exception as e: + self.error_or_exception(f'Exception in rule "{rule}":', e) + if stop: + break + + for _hook_func in self._event_postprocessor_hooks: + await _hook_func(current_event) + + logger.info("Event Finished") + + def _load_rule_class( + self, + rule_class: Type[Rule[Any, Any, Any]], + rule_load_type: RuleLoadType, + rule_file_path: Optional[str], + ) -> None: + """Load a rule class""" + priority = getattr(rule_class, "priority", None) + if isinstance(priority, int) and priority >= 0: + for _rule in self.rules: + if _rule.__name__ == rule_class.__name__: + logger.warning( + f'Already have a same name rule "{_rule.__name__}"' + ) + rule_class.__rule_load_type__ = rule_load_type + rule_class.__rule_file_path__ = rule_file_path + self.rules_priority_dict[priority].append(rule_class) + logger.info( + f'Succeeded to load rule "{rule_class.__name__}" ' + f'from class "{rule_class!r}"' + ) + else: + self.error_or_exception( + f'Load rule from class "{rule_class!r}" failed:', + LoadModuleError( + f'rule priority incorrect in the class "{rule_class!r}"' + ), + ) + + def _load_rules_from_module_name( + self, + module_name: str, + *, + rule_load_type: RuleLoadType, + reload: bool = False, + ) -> None: + """Load rules from the given module.""" + try: + rule_classes = get_classes_from_module_name( + module_name, Rule, reload=reload + ) + except ImportError as e: + self.error_or_exception( + f'Import module "{module_name}" failed:', e) + else: + for rule_class, module in rule_classes: + self._load_rule_class( + rule_class, # type: ignore + rule_load_type, + module.__file__, + ) + + def _load_rules( + self, + *rules: Union[Type[Rule[Any, Any, Any]], str, Path], + rule_load_type: Optional[RuleLoadType] = None, + reload: bool = False, + ) -> None: + for rule_ in rules: + try: + if isinstance(rule_, type) and issubclass(rule_, Rule): + self._load_rule_class( + rule_, rule_load_type or RuleLoadType.CLASS, None + ) + elif isinstance(rule_, str): + logger.info(f'Loading rules from module "{rule_}"') + self._load_rules_from_module_name( + rule_, + rule_load_type=rule_load_type or RuleLoadType.NAME, + reload=reload, + ) + elif isinstance(rule_, Path): + logger.info(f'Loading rules from path "{rule_}"') + if not rule_.is_file(): + raise LoadModuleError( # noqa: TRY301 + f'The rule path "{rule_}" must be a file' + ) + + if rule_.suffix != ".py": + raise LoadModuleError( # noqa: TRY301 + f'The path "{rule_}" must endswith ".py"' + ) + + rule_module_name = None + for path in self._module_path_finder.path: + try: + if rule_.stem == "__init__": + if rule_.resolve().parent.parent.samefile(Path(path)): + rule_module_name = rule_.resolve().parent.name + break + elif rule_.resolve().parent.samefile(Path(path)): + rule_module_name = rule_.stem + break + except OSError: + continue + if rule_module_name is None: + rel_path = rule_.resolve().relative_to(Path().resolve()) + if rel_path.stem == "__init__": + rule_module_name = ".".join(rel_path.parts[:-1]) + else: + rule_module_name = ".".join( + rel_path.parts[:-1] + (rel_path.stem,) + ) + + self._load_rules_from_module_name( + rule_module_name, + rule_load_type=rule_load_type or RuleLoadType.FILE, + reload=reload, + ) + else: + raise TypeError( # noqa: TRY301 + f"{rule_} can not be loaded as rule" + ) + except Exception as e: + self.error_or_exception(f'Load rule "{rule_}" failed:', e) + + def load_rules( + self, *rules: Union[Type[Rule[Any, Any, Any]], str, Path] + ) -> None: + self._extend_rules.extend(rules) + + return self._load_rules(*rules) + + def _load_rules_from_dirs(self, *dirs: Path) -> None: + dir_list = [str(x.resolve()) for x in dirs] + logger.info( + f'Loading rules from dirs "{", ".join(map(str, dir_list))}"') + self._module_path_finder.path.extend(dir_list) + for module_info in pkgutil.iter_modules(dir_list): + if not module_info.name.startswith("_"): + self._load_rules_from_module_name( + module_info.name, rule_load_type=RuleLoadType.DIR + ) + + def load_rules_from_dirs(self, *dirs: Path) -> None: + self._extend_rule_dirs.extend(dirs) + self._load_rules_from_dirs(*dirs) + + def get_rule(self, name: str) -> Type[Rule[Any, Any, Any]]: + for _rule in self.rules: + if _rule.__name__ == name: + return _rule + raise LookupError(f'Can not find rule named "{name}"') + + def error_or_exception( + self, message: str, exception: Exception + ) -> None: # pragma: no cover + if self.config.core.log.verbose_exception: + logger.exception(message) + else: + logger.error(f"{message} {exception!r}") + + def core_run_hook(self, func: CoreHook) -> CoreHook: + self._core_run_hooks.append(func) + return func + + def core_exit_hook(self, func: CoreHook) -> CoreHook: + self._core_exit_hooks.append(func) + return func + + def rule_enable_hook(self, func: RuleHook) -> RuleHook: + self._rule_enable_hooks.append(func) + return func + + def rule_run_hook(self, func: RuleHook) -> RuleHook: + self._rule_run_hooks.append(func) + return func + + def rule_disable_hook(self, func: RuleHook) -> RuleHook: + self._rule_disable_hooks.append(func) + return func + + def event_preprocessor_hook(self, func: EventHook) -> EventHook: + self._event_preprocessor_hooks.append(func) + return func + + def event_postprocessor_hook(self, func: EventHook) -> EventHook: + self._event_postprocessor_hooks.append(func) + return func diff --git a/modules/HydroRollCore/dependencies.py b/modules/HydroRollCore/dependencies.py new file mode 100644 index 0000000..3a662fd --- /dev/null +++ b/modules/HydroRollCore/dependencies.py @@ -0,0 +1,118 @@ +import inspect +from contextlib import AsyncExitStack, asynccontextmanager, contextmanager +from typing import ( + Any, + AsyncContextManager, + AsyncGenerator, + Callable, + ContextManager, + Dict, + Generator, + Optional, + Type, + TypeVar, + Union, + cast, +) + +from .utils import get_annotations, sync_ctx_manager_wrapper + +_T = TypeVar("_T") +Dependency = Union[ + # Class + Type[Union[_T, AsyncContextManager[_T], ContextManager[_T]]], + # GeneratorContextManager + Callable[[], AsyncGenerator[_T, None]], + Callable[[], Generator[_T, None, None]], +] + + +__all__ = ["Depends"] + + +class InnerDepends: + + dependency: Optional[Dependency[Any]] + use_cache: bool + + def __init__( + self, dependency: Optional[Dependency[Any]] = None, *, use_cache: bool = True + ) -> None: + self.dependency = dependency + self.use_cache = use_cache + + def __repr__(self) -> str: + attr = getattr(self.dependency, "__name__", type(self.dependency).__name__) + cache = "" if self.use_cache else ", use_cache=False" + return f"InnerDepends({attr}{cache})" + + +def Depends( # noqa: N802 # pylint: disable=invalid-name + dependency: Optional[Dependency[_T]] = None, *, use_cache: bool = True +) -> _T: + + return InnerDepends(dependency=dependency, use_cache=use_cache) # type: ignore + + +async def solve_dependencies( + dependent: Dependency[_T], + *, + use_cache: bool, + stack: AsyncExitStack, + dependency_cache: Dict[Dependency[Any], Any], +) -> _T: + if use_cache and dependent in dependency_cache: + return dependency_cache[dependent] + + if isinstance(dependent, type): + # type of dependent is Type[T] + values: Dict[str, Any] = {} + ann = get_annotations(dependent) + for name, sub_dependent in inspect.getmembers( + dependent, lambda x: isinstance(x, InnerDepends) + ): + assert isinstance(sub_dependent, InnerDepends) + if sub_dependent.dependency is None: + dependent_ann = ann.get(name, None) + if dependent_ann is None: + raise TypeError("can not solve dependent") + sub_dependent.dependency = dependent_ann + values[name] = await solve_dependencies( + sub_dependent.dependency, + use_cache=sub_dependent.use_cache, + stack=stack, + dependency_cache=dependency_cache, + ) + depend_obj = cast( + Union[_T, AsyncContextManager[_T], ContextManager[_T]], + dependent.__new__(dependent), # pyright: ignore[reportGeneralTypeIssues] + ) + for key, value in values.items(): + setattr(depend_obj, key, value) + depend_obj.__init__() # type: ignore[misc] # pylint: disable=unnecessary-dunder-call + + if isinstance(depend_obj, AsyncContextManager): + depend = await stack.enter_async_context( + depend_obj # pyright: ignore[reportUnknownArgumentType] + ) + elif isinstance(depend_obj, ContextManager): + depend = await stack.enter_async_context( + sync_ctx_manager_wrapper( + depend_obj # pyright: ignore[reportUnknownArgumentType] + ) + ) + else: + depend = depend_obj + elif inspect.isasyncgenfunction(dependent): + # type of dependent is Callable[[], AsyncGenerator[T, None]] + cm = asynccontextmanager(dependent)() + depend = cast(_T, await stack.enter_async_context(cm)) + elif inspect.isgeneratorfunction(dependent): + # type of dependent is Callable[[], Generator[T, None, None]] + cm = sync_ctx_manager_wrapper(contextmanager(dependent)()) + depend = cast(_T, await stack.enter_async_context(cm)) + else: + raise TypeError("dependent is not a class or generator function") + + dependency_cache[dependent] = depend + return depend \ No newline at end of file diff --git a/modules/HydroRollCore/dev/__init__.py b/modules/HydroRollCore/dev/__init__.py new file mode 100644 index 0000000..cf99d7f --- /dev/null +++ b/modules/HydroRollCore/dev/__init__.py @@ -0,0 +1 @@ +from .grps import v1 \ No newline at end of file diff --git a/modules/HydroRollCore/dev/character.py b/modules/HydroRollCore/dev/character.py new file mode 100644 index 0000000..c883e45 --- /dev/null +++ b/modules/HydroRollCore/dev/character.py @@ -0,0 +1,2 @@ +class Character: + class Attribute: ... diff --git a/modules/HydroRollCore/dev/echo.py b/modules/HydroRollCore/dev/echo.py new file mode 100644 index 0000000..7107159 --- /dev/null +++ b/modules/HydroRollCore/dev/echo.py @@ -0,0 +1,15 @@ +"""HydroRoll-Team/echo +水系跨平台事件标准(cross-platform event standard): Event Communication and Harmonization across Online platforms. +:ref: https://github/com/HydroRoll-Team/echo +:ref: https://echo.hydroroll.team +""" + +class WorkFlow(object): + """workflow + :ref: https://echo.hydroroll.team/Event/#1_workflow + """ + +class CallBack(object): + """callback + :ref: https://echo.hydroroll.team/Event/#4_callback + """ \ No newline at end of file diff --git a/modules/HydroRollCore/dev/grps/__init__.py b/modules/HydroRollCore/dev/grps/__init__.py new file mode 100644 index 0000000..bbf8c7e --- /dev/null +++ b/modules/HydroRollCore/dev/grps/__init__.py @@ -0,0 +1 @@ +from . import v1 \ No newline at end of file diff --git a/modules/HydroRollCore/dev/grps/v1.py b/modules/HydroRollCore/dev/grps/v1.py new file mode 100644 index 0000000..5af118c --- /dev/null +++ b/modules/HydroRollCore/dev/grps/v1.py @@ -0,0 +1 @@ +__version__ = "1.0.0-alpha.1" \ No newline at end of file diff --git a/modules/HydroRollCore/doc/__init__.py b/modules/HydroRollCore/doc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRollCore/event.py b/modules/HydroRollCore/event.py new file mode 100644 index 0000000..afdb00c --- /dev/null +++ b/modules/HydroRollCore/event.py @@ -0,0 +1,108 @@ +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Generic, Optional, Union +from typing_extensions import Self + +from pydantic import BaseModel, ConfigDict +from .typing import RuleT + + +class Event(ABC, BaseModel, Generic[RuleT]): + model_config = ConfigDict(extra="allow") + + if TYPE_CHECKING: + rule: RuleT + else: + rule: Any + type: Optional[str] + __handled__: bool = False + + def __str__(self) -> str: + return f"Event<{self.type}>" + + def __repr__(self) -> str: + return self.__str__() + + +class MessageEvent(Event[RuleT], Generic[RuleT]): + """Base class for general message event classes.""" + + @abstractmethod + def get_plain_text(self) -> str: + """Get the plain text content of the message. + + Returns: + The plain text content of the message. + """ + + @abstractmethod + async def reply(self, message: str) -> Any: + """Reply message. + + Args: + message: The content of the reply message. + + Returns: + The response to the reply message action. + """ + + @abstractmethod + async def is_same_sender(self, other: Self) -> bool: + """Determine whether itself and another event are the same sender. + + Args: + other: another event. + + Returns: + Is it the same sender? + """ + + async def get( + self, + *, + max_try_times: Optional[int] = None, + timeout: Optional[Union[int, float]] = None, + ) -> Self: + """Get the user's reply message. + + Equivalent to `get()` of ``Bot``, the condition is that the adapter, event type and sender are the same. + + Args: + max_try_times: Maximum number of events. + timeout: timeout period. + + Returns: + Message event that the user replies to. + + Raises: + GetEventTimeout: Maximum number of events exceeded or timeout. + """ + + return await self.rule.get( + self.is_same_sender, + event_type=type(self), + max_try_times=max_try_times, + timeout=timeout, + ) + + async def ask( + self, + message: str, + max_try_times: Optional[int] = None, + timeout: Optional[Union[int, float]] = None, + ) -> Self: + """Ask for news. + + Indicates getting the user's reply after replying to a message. + Equivalent to executing ``get()`` after ``reply()``. + + Args: + message: The content of the reply message. + max_try_times: Maximum number of events. + timeout: timeout period. + + Returns: + Message event that the user replies to. + """ + + await self.reply(message) + return await self.get(max_try_times=max_try_times, timeout=timeout) diff --git a/modules/HydroRollCore/exceptions.py b/modules/HydroRollCore/exceptions.py new file mode 100644 index 0000000..c71118f --- /dev/null +++ b/modules/HydroRollCore/exceptions.py @@ -0,0 +1,22 @@ +class EventException(BaseException): + ... + + +class SkipException(EventException): + ... + + +class StopException(EventException): + ... + + +class CoreException(Exception): + ... # noqa: N818 + + +class GetEventTimeout(CoreException): + ... + + +class LoadModuleError(CoreException): + ... diff --git a/modules/HydroRollCore/feat/__init__.py b/modules/HydroRollCore/feat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRollCore/log.py b/modules/HydroRollCore/log.py new file mode 100644 index 0000000..dfa126c --- /dev/null +++ b/modules/HydroRollCore/log.py @@ -0,0 +1,25 @@ +import os +import sys +from datetime import datetime + +from loguru import logger as _logger + +logger = _logger + +current_path = os.path.dirname(os.path.abspath("__file__")) +log_path = os.path.join( + current_path, "logs", datetime.now().strftime("%Y-%m-%d") + ".log" +) + + +def error_or_exception(message: str, exception: Exception, verbose: bool): + logger.remove() + logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss.SSS} [{level}] > {name}:{function}:{line} - {message}", + ) + logger.add(sink=log_path, level="INFO", rotation="10 MB") + if verbose: + logger.exception(message) + else: + logger.critical(f"{message} {exception!r}") diff --git a/modules/HydroRollCore/perf/__init__.py b/modules/HydroRollCore/perf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRollCore/py.typed b/modules/HydroRollCore/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRollCore/rule/BaseRule/CharacterCard.py b/modules/HydroRollCore/rule/BaseRule/CharacterCard.py new file mode 100644 index 0000000..2baea48 --- /dev/null +++ b/modules/HydroRollCore/rule/BaseRule/CharacterCard.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + + +@dataclass +class Custom(object): + """Docstring for Custom.""" + + property: type + + +class Attribute(Custom): ... + + +class Skill(Custom): ... + + +class Information(Custom): ... diff --git a/modules/HydroRollCore/rule/BaseRule/CustomRule.py b/modules/HydroRollCore/rule/BaseRule/CustomRule.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRollCore/rule/BaseRule/Wiki.py b/modules/HydroRollCore/rule/BaseRule/Wiki.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/HydroRollCore/rule/BaseRule/__init__.py b/modules/HydroRollCore/rule/BaseRule/__init__.py new file mode 100644 index 0000000..fdde86c --- /dev/null +++ b/modules/HydroRollCore/rule/BaseRule/__init__.py @@ -0,0 +1,3 @@ +from . import CharacterCard # noqa: F401 +from . import CustomRule # noqa: F401 +from . import Wiki # noqa: F401 diff --git a/modules/HydroRollCore/rule/__init__.py b/modules/HydroRollCore/rule/__init__.py new file mode 100644 index 0000000..e144091 --- /dev/null +++ b/modules/HydroRollCore/rule/__init__.py @@ -0,0 +1,164 @@ +import functools # noqa: F401 +from typing import Generic, Any, Type + +from abc import ABC + +from . import BaseRule # noqa: F401 +from ..typing import RuleT # noqa: F401 + +import inspect +from abc import abstractmethod # noqa: F401 +from enum import Enum +from typing import ( + TYPE_CHECKING, + ClassVar, + NoReturn, + Optional, + Tuple, + cast, + final, +) +from typing_extensions import Annotated, get_args, get_origin + +from ..config import ConfigModel + +from ..dependencies import Depends +from ..event import Event +from ..exceptions import SkipException, StopException +from ..typing import ConfigT, EventT, StateT +from ..utils import is_config_class + +if TYPE_CHECKING: + from ..core import Core + + +class RuleLoadType(Enum): + """Rules loaded types.""" + + DIR = "dir" + NAME = "name" + FILE = "file" + CLASS = "class" + + +class Rule(ABC, Generic[EventT, StateT, ConfigT]): + priority: ClassVar[int] = 0 + block: ClassVar[bool] = False + + # Cannot use ClassVar because PEP 526 does not allow it + Config: Type[ConfigT] + + __rule_load_type__: ClassVar[RuleLoadType] + __rule_file_path__: ClassVar[Optional[str]] + + if TYPE_CHECKING: + event: EventT + else: + event = Depends(Event) # noqa: F821 + + def __init_state__(self) -> Optional[StateT]: + """Initialize rule state.""" + + def __init_subclass__( + cls, + config: Optional[Type[ConfigT]] = None, + init_state: Optional[StateT] = None, + **_kwargs: Any, + ) -> None: + super().__init_subclass__() + + orig_bases: Tuple[type, ...] = getattr(cls, "__orig_bases__", ()) + for orig_base in orig_bases: + origin_class = get_origin(orig_base) + if inspect.isclass(origin_class) and issubclass(origin_class, Rule): + try: + _event_t, state_t, config_t = cast( + Tuple[EventT, StateT, ConfigT], get_args(orig_base) + ) + except ValueError: # pragma: no cover + continue + if ( + config is None + and inspect.isclass(config_t) + and issubclass(config_t, ConfigModel) + ): + config = config_t # pyright: ignore + if ( + init_state is None + and get_origin(state_t) is Annotated + and hasattr(state_t, "__metadata__") + ): + init_state = state_t.__metadata__[0] # pyright: ignore + + if not hasattr(cls, "Config") and config is not None: + cls.Config = config + if cls.__init_state__ is Rule.__init_state__ and init_state is not None: + cls.__init_state__ = lambda _: init_state # type: ignore + + @final + @property + def name(self) -> str: + """rule class name.""" + return self.__class__.__name__ + + @final + @property + def core(self) -> "Core": + """core object.""" + return self.event.core # pylint: disable=no-member + + @final + @property + def config(self) -> ConfigT: + """rule configuration.""" + default: Any = None + config_class = getattr(self, "Config", None) + if is_config_class(config_class): + return getattr( + self.core.config.rule, + config_class.__config_name__, + default, + ) + return default + + @final + def stop(self) -> NoReturn: + """Stop propagation of current events.""" + raise StopException + + @final + def skip(self) -> NoReturn: + """Skips itself and continues propagation of the current event.""" + raise SkipException + + @property + def state(self) -> StateT: + """rule status.""" + return self.core.rule_state[self.name] + + @state.setter + @final + def state(self, value: StateT) -> None: + self.core.rule_state[self.name] = value + + async def enable(self): ... + + async def disable(self): ... + + @staticmethod + def aliases(names, ignore_case=False): + def decorator(func): + func._aliases = names + func._ignore_case = ignore_case + return func + + return decorator + + @final + async def safe_run(self) -> None: + try: + await self.enable() + except Exception as e: + self.bot.error_or_exception( + f"Enable rule {self.__class__.__name__} failed:", e + ) diff --git a/modules/HydroRollCore/typing.py b/modules/HydroRollCore/typing.py new file mode 100644 index 0000000..d74fd26 --- /dev/null +++ b/modules/HydroRollCore/typing.py @@ -0,0 +1,20 @@ +# ruff: noqa: TCH001 +from typing import TYPE_CHECKING, Awaitable, Callable, Optional, TypeVar + +if TYPE_CHECKING: + from typing import Any + + from .core import Core + from .config import ConfigModel + from .event import Event + from .rule import Rule + + +StateT = TypeVar("StateT") +EventT = TypeVar("EventT", bound="Event[Any]") +RuleT = TypeVar("RuleT", bound="Rule[Any, Any, Any]") +ConfigT = TypeVar("ConfigT", bound=Optional["ConfigModel"]) + +CoreHook = Callable[["Core"], Awaitable[None]] +RuleHook = Callable[["Rule"], Awaitable[None]] +EventHook = Callable[["Event[Any]"], Awaitable[None]] diff --git a/modules/HydroRollCore/utils.py b/modules/HydroRollCore/utils.py new file mode 100644 index 0000000..e85cea4 --- /dev/null +++ b/modules/HydroRollCore/utils.py @@ -0,0 +1,291 @@ +"""A utility used internally by iamai.""" + +import asyncio +import importlib +import inspect +import json +import os +import os.path +import sys +import traceback +from abc import ABC +from contextlib import asynccontextmanager +from functools import partial +from importlib.abc import MetaPathFinder +from importlib.machinery import ModuleSpec, PathFinder +from types import GetSetDescriptorType, ModuleType +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + Awaitable, + Callable, + ClassVar, + ContextManager, + Coroutine, + Dict, + List, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, + cast, +) +from typing_extensions import ParamSpec, TypeAlias, TypeGuard + +from pydantic import BaseModel + +from .config import ConfigModel +from .typing import EventT + +if TYPE_CHECKING: + from os import PathLike + +__all__ = [ + "ModulePathFinder", + "is_config_class", + "get_classes_from_module", + "get_classes_from_module_name", + "PydanticEncoder", + "samefile", + "sync_func_wrapper", + "sync_ctx_manager_wrapper", + "wrap_get_func", + "get_annotations", +] + +_T = TypeVar("_T") +_P = ParamSpec("_P") +_R = TypeVar("_R") +_TypeT = TypeVar("_TypeT", bound=Type[Any]) + +StrOrBytesPath: TypeAlias = Union[str, bytes, "PathLike[str]", "PathLike[bytes]"] + + +class ModulePathFinder(MetaPathFinder): + """Meta path finder for finding iamai components.""" + + path: ClassVar[List[str]] = [] + + def find_spec( + self, + fullname: str, + path: Optional[Sequence[str]] = None, + target: Optional[ModuleType] = None, + ) -> Union[ModuleSpec, None]: + """Used to find the ``spec`` of a specified module.""" + if path is None: + path = [] + return PathFinder.find_spec(fullname, self.path + list(path), target) + + +def is_config_class(config_class: Any) -> TypeGuard[Type[ConfigModel]]: + return ( + inspect.isclass(config_class) + and issubclass(config_class, ConfigModel) + and isinstance(getattr(config_class, "__config_name__", None), str) + and ABC not in config_class.__bases__ + and not inspect.isabstract(config_class) + ) + + +def get_classes_from_module(module: ModuleType, super_class: _TypeT) -> List[_TypeT]: + """Find a class of the specified type from the module. + + Args: + module: Python module. + super_class: The superclass of the class to be found. + + Returns: + Returns a list of classes that meet the criteria. + """ + classes: List[_TypeT] = [] + for _, module_attr in inspect.getmembers(module, inspect.isclass): + if ( + (inspect.getmodule(module_attr) or module) is module + and issubclass(module_attr, super_class) + and module_attr != super_class + and ABC not in module_attr.__bases__ + and not inspect.isabstract(module_attr) + ): + classes.append(cast(_TypeT, module_attr)) + return classes + + +def get_classes_from_module_name( + name: str, super_class: _TypeT, *, reload: bool = False +) -> List[Tuple[_TypeT, ModuleType]]: + """Find a class of the specified type from the module with the specified name. + + Args: + name: module name, the format is the same as the Python ``import`` statement. + super_class: The superclass of the class to be found. + reload: Whether to reload the module. + + Returns: + Returns a list of tuples consisting of classes and modules that meet the criteria. + + Raises: + ImportError: An error occurred while importing the module. + """ + try: + importlib.invalidate_caches() + module = importlib.import_module(name) + if reload: + importlib.reload(module) + return [(x, module) for x in get_classes_from_module(module, super_class)] + except KeyboardInterrupt: + # Do not capture KeyboardInterrupt + # Catching KeyboardInterrupt will prevent the user from closing Python when the module being imported is stuck in an infinite loop + raise + except BaseException as e: + raise ImportError(e, traceback.format_exc()) from e + + +class PydanticEncoder(json.JSONEncoder): + """``JSONEncoder`` class for parsing ``pydantic.BaseModel``.""" + + def default(self, o: Any) -> Any: + """Returns a serializable object of ``o``.""" + if isinstance(o, BaseModel): + return o.model_dump(mode="json") + return super().default(o) + + +def samefile(path1: StrOrBytesPath, path2: StrOrBytesPath) -> bool: + """A simple wrapper around ``os.path.samefile``. + + Args: + path1: path1. + path2: path 2. + + Returns: + If two paths point to the same file or directory. + """ + try: + return path1 == path2 or os.path.samefile(path1, path2) # noqa: PTH121 + except OSError: + return False + + +def sync_func_wrapper( + func: Callable[_P, _R], *, to_thread: bool = False +) -> Callable[_P, Coroutine[None, None, _R]]: + """Wrap a synchronous function as an asynchronous function. + + Args: + func: synchronous function to be packaged. + to_thread: Whether to run the synchronization function in a separate thread. Defaults to ``False``. + + Returns: + Asynchronous functions. + """ + if to_thread: + + async def _wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: + loop = asyncio.get_running_loop() + func_call = partial(func, *args, **kwargs) + return await loop.run_in_executor(None, func_call) + + else: + + async def _wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R: + return func(*args, **kwargs) + + return _wrapper + + +@asynccontextmanager +async def sync_ctx_manager_wrapper( + cm: ContextManager[_T], *, to_thread: bool = False +) -> AsyncGenerator[_T, None]: + """Wrap a synchronous context manager into an asynchronous context manager. + + Args: + cm: The synchronization context manager to be wrapped. + to_thread: Whether to run the synchronization function in a separate thread. Defaults to ``False``. + + Returns: + Asynchronous context manager. + """ + try: + yield await sync_func_wrapper(cm.__enter__, to_thread=to_thread)() + except Exception as e: + if not await sync_func_wrapper(cm.__exit__, to_thread=to_thread)( + type(e), e, e.__traceback__ + ): + raise + else: + await sync_func_wrapper(cm.__exit__, to_thread=to_thread)(None, None, None) + + +def wrap_get_func( + func: Optional[Callable[[EventT], Union[bool, Awaitable[bool]]]], +) -> Callable[[EventT], Awaitable[bool]]: + """Wrap the parameters accepted by the ``get()`` function into an asynchronous function. + + Args: + func: The parameters accepted by the ``get()`` function. + + Returns: + Asynchronous functions. + """ + if func is None: + return sync_func_wrapper(lambda _: True) + if not asyncio.iscoroutinefunction(func): + return sync_func_wrapper(func) # type: ignore + return func + + +if sys.version_info >= (3, 10): # pragma: no cover + from inspect import get_annotations +else: # pragma: no cover + + def get_annotations( + obj: Union[Callable[..., object], Type[Any], ModuleType], + ) -> Dict[str, Any]: + """Compute the annotation dictionary of an object. + + Args: + obj: A callable object, class, or module. + + Raises: + TypeError: ``obj`` is not a callable object, class or module. + ValueError: Object's ``__annotations__`` is not a dictionary or ``None``. + + Returns: + Annotation dictionary for objects. + """ + ann: Union[Dict[str, Any], None] + + if isinstance(obj, type): + # class + obj_dict = getattr(obj, "__dict__", None) + if obj_dict and hasattr(obj_dict, "get"): + ann = obj_dict.get("__annotations__", None) + if isinstance(ann, GetSetDescriptorType): + ann = None + else: + ann = None + elif isinstance(obj, ModuleType) or callable(obj): + # this includes types.ModuleType, types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, "__annotations__", None) + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError( # noqa: TRY004 + f"{obj!r}.__annotations__ is neither a dict nor None" + ) + + if not ann: + return {} + + return dict(ann) diff --git a/modules/OneRoll/__init__.py b/modules/OneRoll/__init__.py new file mode 100644 index 0000000..bc17a47 --- /dev/null +++ b/modules/OneRoll/__init__.py @@ -0,0 +1,14 @@ +from . import diceast as ast +from . import utils +from .dice import * +from .errors import * +from .expression import * +from .stringifiers import * + +# useful top-level functions to get started quickly +_roller = Roller() +roll = _roller.roll +parse = _roller.parse + + +__version__ = '1.0.2' \ No newline at end of file diff --git a/modules/OneRoll/cli.py b/modules/OneRoll/cli.py new file mode 100644 index 0000000..df64d8c --- /dev/null +++ b/modules/OneRoll/cli.py @@ -0,0 +1,19 @@ +from oneroll import roll + + +class Cli: + def parser(): + while True: + try: + roll_result = roll(input(), allow_comments=True) + print( + f"""============== +{roll_result}, +, +, + +{roll_result.expr} +==============""".strip() + ) + except Exception as e: + print(e) diff --git a/modules/OneRoll/dice.py b/modules/OneRoll/dice.py new file mode 100644 index 0000000..56f32f0 --- /dev/null +++ b/modules/OneRoll/dice.py @@ -0,0 +1,276 @@ +from enum import IntEnum +from typing import Callable, Mapping, MutableMapping, Optional, Type, TypeVar, Union + +import cachetools +import lark + +from . import diceast as ast, utils +from .errors import * +from .expression import * +from .stringifiers import MarkdownStringifier, Stringifier + +__all__ = ("CritType", "AdvType", "RollContext", "RollResult", "Roller") + +POSSIBLE_COMMENT_AMBIGUITIES = {"*", } + +TreeType = TypeVar('TreeType', bound=ast.ChildMixin) +ASTNode = TypeVar('ASTNode', bound=ast.Node) +ExpressionNode = TypeVar('ExpressionNode', bound=Number) + + +class CritType(IntEnum): + """ + Integer enumeration representing the crit type of a roll. + """ + NONE = 0 + CRIT = 1 + FAIL = 2 + + +class AdvType(IntEnum): + """ + Integer enumeration representing at what advantage a roll should be made at. + """ + NONE = 0 + ADV = 1 + DIS = -1 + + +class RollContext: + """ + A class to track information about rolls to ensure all rolls halt eventually. + + To use this class, pass an instance to the constructor of :class:`d20.Roller`. + """ + + def __init__(self, max_rolls=1000): + self.max_rolls = max_rolls + self.rolls = 0 + self.reset() + + def reset(self): + """Called at the start of each new roll.""" + self.rolls = 0 + + def count_roll(self, n=1): + """ + Called each time a die is about to be rolled. + + :param int n: The number of rolls about to be made. + :raises d20.TooManyRolls: if the roller should stop rolling dice because too many have been rolled. + """ + self.rolls += n + if self.rolls > self.max_rolls: + raise TooManyRolls("Too many dice rolled.") + + +class RollResult: + """ + Holds information about the result of a roll. This should generally not be constructed manually. + """ + + def __init__(self, the_ast: ASTNode, the_roll: Expression, stringifier: Stringifier): + """ + :type the_ast: ast.Node + :type the_roll: d20.Expression + :type stringifier: d20.Stringifier + """ + self.ast = the_ast + self.expr = the_roll + self.stringifier = stringifier + self.comment = the_roll.comment + + @property + def total(self) -> int: + return int(self.expr.total) + + @property + def result(self) -> str: + return self.stringifier.stringify(self.expr) + + @property + def crit(self) -> CritType: + """ + If the leftmost node was Xd20kh1, returns :class:`CritType.CRIT` if the roll was a 20 and + :class:`CritType.FAIL` if the roll was a 1. + Returns :class:`CritType.NONE` otherwise. + + :rtype: CritType + """ + # find the left most node in the dice expression + left = self.expr + while left.children: + left = left.children[0] + + # ensure the node is dice + if not isinstance(left, Dice): + return CritType.NONE + + # ensure only one die of size 20 is kept + if not (len(left.keptset) == 1 and left.size == 20): + return CritType.NONE + + if left.total == 1: + return CritType.FAIL + elif left.total == 20: + return CritType.CRIT + return CritType.NONE + + def __str__(self): + return self.result + + def __int__(self): + return self.total + + def __float__(self): + return self.expr.total + + def __repr__(self): + return f"" + + +# noinspection PyMethodMayBeStatic +class Roller: + """The main class responsible for parsing dice into an AST and evaluating that AST.""" + + def __init__(self, context: Optional[RollContext] = None): + if context is None: + context = RollContext() + + self._nodes: Mapping[Type[ASTNode], Callable[[ASTNode], ExpressionNode]] = { + ast.Expression: self._eval_expression, + ast.AnnotatedNumber: self._eval_annotatednumber, + ast.Literal: self._eval_literal, + ast.Parenthetical: self._eval_parenthetical, + ast.UnOp: self._eval_unop, + ast.BinOp: self._eval_binop, + ast.OperatedSet: self._eval_operatedset, + ast.NumberSet: self._eval_numberset, + ast.OperatedDice: self._eval_operateddice, + ast.Dice: self._eval_dice + } + self._parse_cache: MutableMapping[str, ASTNode] = cachetools.LFUCache(256) + self.context: RollContext = context + + def roll(self, + expr: Union[str, ASTNode], + stringifier: Optional[Stringifier] = None, + allow_comments: bool = False, + advantage: AdvType = AdvType.NONE) -> RollResult: + """ + Rolls the dice. + + :param expr: The dice to roll. + :type expr: str or ast.Node + :param stringifier: The stringifier to stringify the result. Defaults to MarkdownStringifier. + :type stringifier: d20.Stringifier + :param bool allow_comments: Whether to parse for comments after the main roll expression (potential slowdown) + :param AdvType advantage: If the roll should be made at advantage. Only applies if the leftmost node is 1d20. + :rtype: RollResult + """ + if stringifier is None: + stringifier = MarkdownStringifier() + + self.context.reset() + + if isinstance(expr, str): # is this a preparsed tree? + dice_tree = self.parse(expr, allow_comments) + else: + dice_tree = expr + + if advantage != AdvType.NONE: + dice_tree = utils.ast_adv_copy(dice_tree, advantage) + + dice_expr = self._eval(dice_tree) + return RollResult(dice_tree, dice_expr, stringifier) + + # parsers + def parse(self, expr: str, allow_comments: bool = False) -> ast.Expression: + """ + Parses a dice expression into an AST. + + :param expr: The dice to roll. + :type expr: str + :param bool allow_comments: Whether to parse for comments after the main roll expression (potential slowdown) + :rtype: ast.Expression + """ + try: + if not allow_comments: + return self._parse_no_comment(expr) + else: + return self._parse_with_comments(expr) + except lark.UnexpectedToken as ut: + raise RollSyntaxError(ut.line, ut.column, ut.token, ut.expected) + except lark.UnexpectedCharacters as uc: + raise RollSyntaxError(uc.line, uc.column, expr[uc.pos_in_stream], uc.allowed) + + def _parse_no_comment(self, expr: str) -> ast.Expression: + # see if this expr is in cache + clean_expr = expr.replace(' ', '') + if clean_expr in self._parse_cache: + return self._parse_cache[clean_expr] + dice_tree = ast.parser.parse(expr, start='expr') + self._parse_cache[clean_expr] = dice_tree + return dice_tree + + def _parse_with_comments(self, expr: str) -> ast.Expression: + try: + return ast.parser.parse(expr, start='commented_expr') + except lark.UnexpectedInput as ui: + # if the statement up to the unexpected token ends with an operator, remove that from the end + successful_fragment = expr[:ui.pos_in_stream] + for op in POSSIBLE_COMMENT_AMBIGUITIES: + if successful_fragment.endswith(op): + successful_fragment = successful_fragment[:-len(op)] + force_comment = expr[len(successful_fragment):] + break + else: + raise + # and parse again (handles edge cases like "1d20 keep the dragon grappled") + result = ast.parser.parse(successful_fragment, start='commented_expr') + result.comment = force_comment + return result + + # evaluator + def _eval(self, node: ASTNode) -> ExpressionNode: + # noinspection PyUnresolvedReferences + # for some reason pycharm thinks this isn't a valid dict operation + handler = self._nodes[type(node)] + return handler(node) + + def _eval_expression(self, node: ast.Expression) -> Expression: + return Expression(self._eval(node.roll), node.comment) + + def _eval_annotatednumber(self, node: ast.AnnotatedNumber) -> ExpressionNode: + target = self._eval(node.value) + target.annotation = ''.join(node.annotations) + return target + + def _eval_literal(self, node: ast.Literal) -> Literal: + return Literal(node.value) + + def _eval_parenthetical(self, node: ast.Parenthetical) -> Parenthetical: + return Parenthetical(self._eval(node.value)) + + def _eval_unop(self, node: ast.UnOp) -> UnOp: + return UnOp(node.op, self._eval(node.value)) + + def _eval_binop(self, node: ast.BinOp) -> BinOp: + return BinOp(self._eval(node.left), node.op, self._eval(node.right)) + + def _eval_operatedset(self, node: ast.OperatedSet) -> ExpressionNode: + target = self._eval(node.value) + for op in node.operations: + the_op = SetOperator.from_ast(op) + the_op.operate(target) + target.operations.append(the_op) + return target + + def _eval_numberset(self, node: ast.NumberSet) -> Set: + return Set([self._eval(n) for n in node.values]) + + def _eval_operateddice(self, node: ast.OperatedDice) -> ExpressionNode: + return self._eval_operatedset(node) + + def _eval_dice(self, node: ast.Dice) -> Dice: + return Dice.new(node.num, node.size, context=self.context) diff --git a/modules/OneRoll/diceast.py b/modules/OneRoll/diceast.py new file mode 100644 index 0000000..6648bd3 --- /dev/null +++ b/modules/OneRoll/diceast.py @@ -0,0 +1,452 @@ +import abc +import os + +from lark import Lark, Token, Transformer + + +# ===== transformer, parser -> ast ===== + +# noinspection PyMethodMayBeStatic +# shush, you +class RollTransformer(Transformer): + _comma = object() + + def expr(self, num): + return Expression(*num) + + def commented_expr(self, numcomment): + return Expression(*numcomment) + + def comparison(self, binop): + return BinOp(*binop) + + def a_num(self, binop): + return BinOp(*binop) + + def m_num(self, binop): + return BinOp(*binop) + + def u_num(self, unop): + return UnOp(*unop) + + def numexpr(self, num_anno): + return AnnotatedNumber(*num_anno) + + def literal(self, num): + return Literal(*num) + + def set(self, opset): + return OperatedSet(*opset) + + def set_op(self, opsel): + return SetOperator.new(*opsel) + + def setexpr(self, the_set): + if len(the_set) == 1 and the_set[-1] is not self._comma: + return Parenthetical(the_set[0]) + elif len(the_set) and the_set[-1] is self._comma: + the_set = the_set[:-1] + return NumberSet(the_set) + + def dice(self, opdice): + return OperatedDice(*opdice) + + def dice_op(self, opsel): + return SetOperator.new(*opsel) + + def diceexpr(self, dice): + if len(dice) == 1: + return Dice(1, *dice) + return Dice(*dice) + + def selector(self, sel): + return SetSelector(*sel) + + def comma(self, _): + return self._comma + + +# ===== helper mixin ===== +class ChildMixin: + """A mixin that tree nodes must implement to support tree traversal utilities.""" + + @property + def children(self): + """ + Returns a list of this node's roll children. + + :rtype: list of ChildMixin + """ + raise NotImplementedError + + @property + def left(self): + """ + Returns the node's leftmost child, or None if there are no children. + + :rtype: ChildMixin or None + """ + return self.children[0] if self.children else None + + @left.setter + def left(self, value): + self.set_child(0, value) + + @property + def right(self): + """ + Returns the node's rightmost child, or None if there are no children. + + :rtype: ChildMixin or None + """ + return self.children[-1] if self.children else None + + @right.setter + def right(self, value): + self.set_child(-1, value) + + def _child_set_check(self, index): + if index > (len(self.children) - 1) or index < -len(self.children): + raise IndexError + + def set_child(self, index, value): + """ + Sets the ith child of this object. + + :param int index: The index of the value to set. + :param value: The new value to set it to. + :type value: ChildMixin + """ + self._child_set_check(index) + raise NotImplementedError + + +# ===== ast classes ===== +class Node(abc.ABC, ChildMixin): + """ + The base class for all AST nodes. + + A Node has no specific attributes, but supports all the methods in :class:`~d20.ast.ChildMixin` for traversal. + """ + + # overridden here for type checking + def set_child(self, index, value): + """ + Sets the ith child of this Node. + + :param int index: Which child to set. + :param value: The Node to set it to. + :type value: Node + """ + super().set_child(index, value) + + @property + def children(self): + """:rtype: list of Node""" + raise NotImplementedError + + def __str__(self): + raise NotImplementedError + + +class Expression(Node): # expr + """Expressions are usually the root of all ASTs.""" + __slots__ = ("roll", "comment") + + def __init__(self, roll, comment=None): + self.roll = roll + self.comment = str(comment) if comment is not None else None + + @property + def children(self): + return [self.roll] + + def set_child(self, index, value): + self._child_set_check(index) + self.roll = value + + def __str__(self): + if self.comment: + return f"{str(self.roll)} {self.comment}" + return str(self.roll) + + +class AnnotatedNumber(Node): # numexpr + """Represents a value with an annotation.""" + __slots__ = ("value", "annotations") + + def __init__(self, value, *annotations): + """ + :type value: Node + :type annotations: lark.Token or str + """ + super().__init__() + self.value = value + self.annotations = [str(a).strip() for a in annotations] + + @property + def children(self): + return [self.value] + + def set_child(self, index, value): + self._child_set_check(index) + self.value = value + + def __str__(self): + return f"{str(self.value)} {''.join(self.annotations)}" + + +class Literal(Node): # literal + __slots__ = ("value",) + + def __init__(self, value): + """ + :type value: lark.Token or int or float + """ + super().__init__() + if isinstance(value, Token): + self.value = int(value) if value.type == 'INTEGER' else float(value) + else: + self.value = value + + @property + def children(self): + return [] + + def __str__(self): + return str(self.value) + + +class Parenthetical(Node): + __slots__ = ("value",) + + def __init__(self, value): + """ + :type value: Node + """ + super().__init__() + self.value = value + + @property + def children(self): + return [self.value] + + def set_child(self, index, value): + self._child_set_check(index) + self.value = value + + def __str__(self): + return f"({str(self.value)})" + + +class UnOp(Node): # u_num + __slots__ = ("op", "value") + + def __init__(self, op, value): + """ + :type op: lark.Token or str + :type value: Node + """ + super().__init__() + self.op = str(op) + self.value = value + + @property + def children(self): + return [self.value] + + def set_child(self, index, value): + self._child_set_check(index) + self.value = value + + def __str__(self): + return f"{self.op}{str(self.value)}" + + +class BinOp(Node): # a_num, m_num + __slots__ = ("op", "left", "right") + + def __init__(self, left, op, right): + """ + :type op: lark.Token or str + :type left: Node + :type right: Node + """ + super().__init__() + self.op = str(op) + self.left = left + self.right = right + + @property + def children(self): + return [self.left, self.right] + + def set_child(self, index, value): + self._child_set_check(index) + if self.children[index] is self.left: + self.left = value + else: + self.right = value + + def __str__(self): + return f"{str(self.left)} {self.op} {str(self.right)}" + + +class SetOperator: # set_op, dice_op + __slots__ = ("op", "sels") + + IMMEDIATE = {"mi", "ma"} + + def __init__(self, op, sels): + """ + :type op: lark.Token or str + :type sels: list of SetSelector + """ + self.op = str(op) + self.sels = sels + + @classmethod + def new(cls, op, sel): + return cls(op, [sel]) + + def add_sels(self, sels): + self.sels.extend(sels) + + def __str__(self): + return "".join([f"{self.op}{str(sel)}" for sel in self.sels]) + + +class SetSelector: # selector + __slots__ = ("cat", "num") + + def __init__(self, cat, num): + """ + :type cat: str or lark.Token or None + :type num: int + """ + self.cat = str(cat) if cat is not None else None + self.num = int(num) + + def __str__(self): + if self.cat: + return f"{self.cat}{self.num}" + return str(self.num) + + +class OperatedSet(Node): # set + __slots__ = ("value", "operations") + + def __init__(self, the_set, *operations): + """ + :type the_set: NumberSet or Dice + :type operations: SetOperator + """ + super().__init__() + self.value = the_set + self.operations = list(operations) + self._simplify_operations() + + @property + def children(self): + return [self.value] + + def set_child(self, index, value): + self._child_set_check(index) + self.value = value + + def _simplify_operations(self): + """Simplifies expressions like k1k2k3 into k(1,2,3).""" + new_operations = [] + + for operation in self.operations: + if operation.op in SetOperator.IMMEDIATE or not new_operations: + new_operations.append(operation) + else: + last_op = new_operations[-1] + if operation.op == last_op.op: + last_op.add_sels(operation.sels) + else: + new_operations.append(operation) + + self.operations = new_operations + + def __str__(self): + return f"{str(self.value)}{''.join([str(op) for op in self.operations])}" + + +class NumberSet(Node): # setexpr + __slots__ = ("values",) + + def __init__(self, values): + """ + :type values: list of Node + """ + super().__init__() + self.values = list(values) + + @property + def children(self): + return self.values + + def set_child(self, index, value): + self._child_set_check(index) + self.values[index] = value + + def __str__(self): + out = f"{', '.join([str(v) for v in self.values])}" + if len(self.values) == 1: + return f"({out},)" + return f"({out})" + + def __copy__(self): + # we need to take a copy of the values list as well + return NumberSet(values=self.values.copy()) + + +class OperatedDice(OperatedSet): # dice + __slots__ = () + + def __init__(self, the_dice, *operations): + """ + :type the_dice: Dice + :type operations: SetOperator + """ + super().__init__(the_dice, *operations) + + +class Dice(Node): # diceexpr + __slots__ = ("num", "size") + + def __init__(self, num, size): + """ + :type num: lark.Token or int + :type size: lark.Token or int or str + """ + super().__init__() + self.num = int(num) + if str(size) == "%": + self.size = str(size) + else: + self.size = int(size) + + @property + def children(self): + return [] + + def __str__(self): + return f"{self.num}d{self.size}" + + +with open(os.path.join(os.path.dirname(__file__), 'grammar.lark')) as f: + grammar = f.read() +parser = Lark(grammar, start=['expr', 'commented_expr'], parser='lalr', transformer=RollTransformer(), + maybe_placeholders=True) + +if __name__ == '__main__': + while True: + parser = Lark(grammar, start=['expr', 'commented_expr'], parser='lalr', maybe_placeholders=True) + result = parser.parse(input(), start='expr') + print(result.pretty()) + print(result) + expr = RollTransformer().transform(result) + print(str(expr)) diff --git a/modules/OneRoll/errors.py b/modules/OneRoll/errors.py new file mode 100644 index 0000000..e3efb52 --- /dev/null +++ b/modules/OneRoll/errors.py @@ -0,0 +1,32 @@ +__all__ = ("RollError", "RollSyntaxError", "RollValueError", "TooManyRolls") + + +class RollError(Exception): + """Generic exception happened in the roll. Base exception for all library exceptions.""" + + def __init__(self, msg): + super().__init__(msg) + + +class RollSyntaxError(RollError): + """Syntax error happened while parsing roll.""" + + def __init__(self, line, col, got, expected): + self.line = line + self.col = col + self.got = got + self.expected = expected + + msg = f"Unexpected input on line {line}, col {col}: expected {', '.join([str(ex) for ex in expected])}, " \ + f"got {str(got)}" + super().__init__(msg) + + +class RollValueError(RollError): + """A bad value was passed to an operator.""" + pass + + +class TooManyRolls(RollError): + """Too many dice rolled (in an individual dice or in rerolls).""" + pass diff --git a/modules/OneRoll/expression.py b/modules/OneRoll/expression.py new file mode 100644 index 0000000..11d1498 --- /dev/null +++ b/modules/OneRoll/expression.py @@ -0,0 +1,634 @@ +import abc +import random + +from . import diceast as ast +from . import errors + +__all__ = ( + "Number", "Expression", "Literal", "UnOp", "BinOp", "Parenthetical", "Set", "Dice", "Die", + "SetOperator", "SetSelector" +) + + +# ===== ast -> expression models ===== +class Number(abc.ABC, ast.ChildMixin): # num + """ + The base class for all expression objects. + + Note that Numbers implement all the methods of a :class:`~d20.ast.ChildMixin`. + """ + + __slots__ = ("kept", "annotation") + + def __init__(self, kept=True, annotation=None): + self.kept = kept + self.annotation = annotation + + @property + def number(self): + """ + Returns the numerical value of this object. + + :rtype: int or float + """ + return sum(n.number for n in self.keptset) + + @property + def total(self): + """ + Returns the numerical value of this object with respect to whether it's kept. + Generally, this is preferred to use over ``number``, as this will return 0 if + the number node was dropped. + + :rtype: int or float + """ + return self.number if self.kept else 0 + + @property + def set(self): + """ + Returns the set representation of this object. + + :rtype: list[Number] + """ + raise NotImplementedError + + @property + def keptset(self): + """ + Returns the set representation of this object, but only including children whose values + were not dropped. + + :rtype: list[Number] + """ + return [n for n in self.set if n.kept] + + def drop(self): + """ + Makes the value of this Number node not count towards a total. + """ + self.kept = False + + def __int__(self): + return int(self.total) + + def __float__(self): + return float(self.total) + + def __repr__(self): + return f"" + + # overridden methods for typechecking + def set_child(self, index, value): + """ + Sets the ith child of this Number. + + :param int index: Which child to set. + :param value: The Number to set it to. + :type value: Number + """ + super().set_child(index, value) + + @property + def children(self): + """:rtype: list[Number]""" + raise NotImplementedError + + +class Expression(Number): + """Expressions are usually the root of all Number trees.""" + __slots__ = ("roll", "comment") + + def __init__(self, roll, comment, **kwargs): + """ + :type roll: Number + """ + super().__init__(**kwargs) + self.roll = roll + self.comment = comment + + @property + def number(self): + return self.roll.number + + @property + def set(self): + return self.roll.set + + @property + def children(self): + return [self.roll] + + def set_child(self, index, value): + self._child_set_check(index) + self.roll = value + + def __repr__(self): + return f"" + + +class Literal(Number): + """A literal integer or float.""" + __slots__ = ("values", "exploded") + + def __init__(self, value, **kwargs): + """ + :type value: int or float + """ + super().__init__(**kwargs) + self.values = [value] # history is tracked to support mi/ma op + self.exploded = False + + @property + def number(self): + return self.values[-1] + + @property + def set(self): + return [self] + + @property + def children(self): + return [] + + def explode(self): + self.exploded = True + + def update(self, value): + """ + :type value: int or float + """ + self.values.append(value) + + def __repr__(self): + return f"" + + +class UnOp(Number): + """Represents a unary operation.""" + __slots__ = ("op", "value") + + UNARY_OPS = { + "-": lambda v: -v, + "+": lambda v: +v + } + + def __init__(self, op, value, **kwargs): + """ + :type op: str + :type value: Number + """ + super().__init__(**kwargs) + self.op = op + self.value = value + + @property + def number(self): + return self.UNARY_OPS[self.op](self.value.total) + + @property + def set(self): + return [self] + + @property + def children(self): + return [self.value] + + def set_child(self, index, value): + self._child_set_check(index) + self.value = value + + def __repr__(self): + return f"" + + +class BinOp(Number): + """Represents a binary operation.""" + __slots__ = ("op", "left", "right") + + BINARY_OPS = { + "+": lambda l, r: l + r, + "-": lambda l, r: l - r, + "*": lambda l, r: l * r, + "/": lambda l, r: l / r, + "//": lambda l, r: l // r, + "%": lambda l, r: l % r, + "<": lambda l, r: int(l < r), + ">": lambda l, r: int(l > r), + "==": lambda l, r: int(l == r), + ">=": lambda l, r: int(l >= r), + "<=": lambda l, r: int(l <= r), + "!=": lambda l, r: int(l != r), + } + + def __init__(self, left, op, right, **kwargs): + """ + :type op: str + :type left: Number + :type right: Number + """ + super().__init__(**kwargs) + self.op = op + self.left = left + self.right = right + + @property + def number(self): + try: + return self.BINARY_OPS[self.op](self.left.total, self.right.total) + except ZeroDivisionError: + raise errors.RollValueError("Cannot divide by zero.") + + @property + def set(self): + return [self] + + @property + def children(self): + return [self.left, self.right] + + def set_child(self, index, value): + self._child_set_check(index) + if self.children[index] is self.left: + self.left = value + else: + self.right = value + + def __repr__(self): + return f"" + + +class Parenthetical(Number): + """Represents a value inside parentheses.""" + __slots__ = ("value", "operations") + + def __init__(self, value, operations=None, **kwargs): + """ + :type value: Number + :type operations: list[SetOperator] + """ + super().__init__(**kwargs) + if operations is None: + operations = [] + self.value = value + self.operations = operations + + @property + def total(self): + return self.value.total if self.kept else 0 + + @property + def set(self): + return self.value.set + + @property + def children(self): + return [self.value] + + def set_child(self, index, value): + self._child_set_check(index) + self.value = value + + def __repr__(self): + return f"" + + +class Set(Number): + """Represents a set of values.""" + __slots__ = ("values", "operations") + + def __init__(self, values, operations=None, **kwargs): + """ + :type values: list[Number] + :type operations: list[SetOperator] + """ + super().__init__(**kwargs) + if operations is None: + operations = [] + self.values = values + self.operations = operations + + @property + def set(self): + return self.values + + @property + def children(self): + return self.values + + def set_child(self, index, value): + self._child_set_check(index) + self.values[index] = value + + def __repr__(self): + return f"" + + def __copy__(self): + return Set(values=self.values.copy(), operations=self.operations.copy()) + + +class Dice(Set): + """A set of Die.""" + __slots__ = ("num", "size", "_context") + + def __init__(self, num, size, values, operations=None, context=None, **kwargs): + """ + :type num: int + :type size: int|str + :type values: list of Die + :type operations: list[SetOperator] + :type context: dice.RollContext + """ + super().__init__(values, operations, **kwargs) + self.num = num + self.size = size + self._context = context + + @classmethod + def new(cls, num, size, context=None): + return cls(num, size, [Die.new(size, context=context) for _ in range(num)], context=context) + + def roll_another(self): + self.values.append(Die.new(self.size, context=self._context)) + + @property + def children(self): + return [] + + def __repr__(self): + return f"" + + def __copy__(self): + return Dice(num=self.num, size=self.size, context=self._context, + values=self.values.copy(), operations=self.operations.copy(), ) + + +class Die(Number): # part of diceexpr + """Represents a single die.""" + __slots__ = ("size", "values", "_context") + + def __init__(self, size, values, context=None): + """ + :type size: int + :type values: list of Literal + :type context: dice.RollContext + """ + super().__init__() + self.size = size + self.values = values + self._context = context + + @classmethod + def new(cls, size, context=None): + inst = cls(size, [], context=context) + inst._add_roll() + return inst + + @property + def number(self): + return self.values[-1].total + + @property + def set(self): + return [self.values[-1]] + + @property + def children(self): + return [] + + def _add_roll(self): + if self.size != '%' and self.size < 1: + raise errors.RollValueError("Cannot roll a 0-sided die.") + if self._context: + self._context.count_roll() + if self.size == '%': + n = Literal(random.randrange(0, 100, 10)) + else: + n = Literal(random.randrange(self.size) + 1) # 200ns faster than randint(1, self._size) + self.values.append(n) + + def reroll(self): + if self.values: + self.values[-1].drop() + self._add_roll() + + def explode(self): + if self.values: + self.values[-1].explode() + # another Die is added by the explode operator + + def force_value(self, new_value): + if self.values: + self.values[-1].update(new_value) + + def __repr__(self): + return f"" + + +# noinspection PyUnresolvedReferences +# selecting on Dice will always return Die +class SetOperator: # set_op, dice_op + """Represents an operation on a set.""" + __slots__ = ("op", "sels") + + def __init__(self, op, sels): + """ + :type op: str + :type sels: list[SetSelector] + """ + self.op = op + self.sels = sels + + @classmethod + def from_ast(cls, node): + return cls(node.op, [SetSelector.from_ast(n) for n in node.sels]) + + def select(self, target, max_targets=None): + """ + Selects the operands in a target set. + + :param target: The source of the operands. + :type target: Number + :param max_targets: The maximum number of targets to select. + :type max_targets: Optional[int] + """ + out = set() + for selector in self.sels: + batch_max = None + if max_targets is not None: + batch_max = max_targets - len(out) + if batch_max == 0: + break + + out.update(selector.select(target, max_targets=batch_max)) + return out + + def operate(self, target): + """ + Operates in place on the values in a target set. + + :param target: The source of the operands. + :type target: Number + """ + operations = { + "k": self.keep, + "p": self.drop, + # dice only + "rr": self.reroll, + "ro": self.reroll_once, + "ra": self.explode_once, + "e": self.explode, + "mi": self.minimum, + "ma": self.maximum + } + + operations[self.op](target) + + def keep(self, target): + """ + :type target: Set + """ + for value in target.keptset: + if value not in self.select(target): + value.drop() + + def drop(self, target): + """ + :type target: Set + """ + for value in self.select(target): + value.drop() + + def reroll(self, target): + """ + :type target: Dice + """ + to_reroll = self.select(target) + while to_reroll: + for die in to_reroll: + die.reroll() + + to_reroll = self.select(target) + + def reroll_once(self, target): + """ + :type target: Dice + """ + for die in self.select(target): + die.reroll() + + def explode(self, target): + """ + :type target: Dice + """ + to_explode = self.select(target) + already_exploded = set() + + while to_explode: + for die in to_explode: + die.explode() + target.roll_another() + + already_exploded.update(to_explode) + to_explode = self.select(target).difference(already_exploded) + + def explode_once(self, target): + """ + :type target: Dice + """ + for die in self.select(target, max_targets=1): + die.explode() + target.roll_another() + + def minimum(self, target): # immediate + """ + :type target: Dice + """ + selector = self.sels[-1] + if selector.cat is not None: + raise errors.RollValueError(f"{str(selector)} is not a valid selector for minimums.") + the_min = selector.num + for die in target.keptset: + if die.number < the_min: + die.force_value(the_min) + + def maximum(self, target): # immediate + """ + :type target: Dice + """ + selector = self.sels[-1] + if selector.cat is not None: + raise errors.RollValueError(f"{str(selector)} is not a valid selector for maximums.") + the_max = selector.num + for die in target.keptset: + if die.number > the_max: + die.force_value(the_max) + + def __str__(self): + return "".join([f"{self.op}{str(sel)}" for sel in self.sels]) + + def __repr__(self): + return f"" + + +class SetSelector: # selector + """Represents a selection on a set.""" + __slots__ = ("cat", "num") + + def __init__(self, cat, num): + """ + :type cat: str or None + :type num: int + """ + self.cat = cat + self.num = num + + @classmethod + def from_ast(cls, node): + return cls(node.cat, node.num) + + def select(self, target, max_targets=None): + """ + Selects operands from a target set. + + :param target: The source of the operands. + :type target: Number + :param int max_targets: The maximum number of targets to select. + :return: The targets in the set. + :rtype: set of Number + """ + selectors = { + "l": self.lowestn, + "h": self.highestn, + "<": self.lessthan, + ">": self.morethan, + None: self.literal + } + + selected = selectors[self.cat](target) + if max_targets is not None: + selected = selected[:max_targets] + return set(selected) + + def lowestn(self, target): + return sorted(target.keptset, key=lambda n: n.total)[:self.num] + + def highestn(self, target): + return sorted(target.keptset, key=lambda n: n.total, reverse=True)[:self.num] + + def lessthan(self, target): + return [n for n in target.keptset if n.total < self.num] + + def morethan(self, target): + return [n for n in target.keptset if n.total > self.num] + + def literal(self, target): + return [n for n in target.keptset if n.total == self.num] + + def __str__(self): + if self.cat: + return f"{self.cat}{self.num}" + return str(self.num) + + def __repr__(self): + return f"" diff --git a/modules/OneRoll/grammar.lark b/modules/OneRoll/grammar.lark new file mode 100644 index 0000000..d45f212 --- /dev/null +++ b/modules/OneRoll/grammar.lark @@ -0,0 +1,60 @@ +// Dice rolling grammar - Avrae spec + +expr: num +commented_expr: num COMMENT? +// ^ starting node for commented rolls + +// comments are given -1 priority - only match comment if no other possibilities +COMMENT.-1: _WS? /.+/ + +// math and operators, PMDAS +?num: comparison + +?comparison: (comparison COMP_OPERATOR _WS?)? a_num _WS? +COMP_OPERATOR: "==" | ">=" | "<=" | "!=" | "<" | ">" + +?a_num: (a_num A_OP _WS?)? m_num _WS? +A_OP: "+" | "-" + +?m_num: (m_num M_OP _WS?)? u_num _WS? +M_OP: "*" | "//" | "/" | "%" + +?u_num: numexpr | U_OP _WS? u_num +U_OP: "+" | "-" + +// numbers +?numexpr: (dice | set | literal) _WS? ANNOTATION* + +ANNOTATION: /\[.*?\]/ _WS? + +literal: INTEGER | DECIMAL + +// sets +?set: setexpr set_op* + +set_op: SET_OPERATOR selector +SET_OPERATOR: "k" | "p" + +setexpr: "(" _WS? (num (_WS? "," _WS? num)* _WS? comma? _WS?)? _WS? ")" +comma: "," + +// dice +?dice: diceexpr dice_op* + +dice_op: (DICE_OPERATOR | SET_OPERATOR) selector +DICE_OPERATOR: "rr" | "ro" | "ra" | "e" | "mi" | "ma" + +diceexpr: INTEGER? "d" DICE_VALUE + +DICE_VALUE: INTEGER | "%" + +selector: [SELTYPE] INTEGER + +SELTYPE: "l" | "h" | "<" | ">" + +// whitespace +_WS: /[ \t\f\r\n]/+ + +// useful constants +%import common.INT -> INTEGER +%import common.DECIMAL diff --git a/modules/OneRoll/stringifiers.py b/modules/OneRoll/stringifiers.py new file mode 100644 index 0000000..0f4587d --- /dev/null +++ b/modules/OneRoll/stringifiers.py @@ -0,0 +1,198 @@ +import abc +from typing import Callable, Iterable, Mapping, Type, TypeVar + +from .expression import * + +__all__ = ("Stringifier", "SimpleStringifier", "MarkdownStringifier") + +ExpressionNode = TypeVar('ExpressionNode', bound=Number) + + +class Stringifier(abc.ABC): + """ + ABC for string builder from dice result. + Children should implement all ``_str_*`` methods to transform an Expression into a str. + """ + + def __init__(self): + self._nodes: Mapping[Type[ExpressionNode], Callable[[ExpressionNode], str]] = { + Expression: self._str_expression, + Literal: self._str_literal, + UnOp: self._str_unop, + BinOp: self._str_binop, + Parenthetical: self._str_parenthetical, + Set: self._str_set, + Dice: self._str_dice, + Die: self._str_die + } + + def stringify(self, the_roll: ExpressionNode) -> str: + """ + Transforms a rolled expression into a string recursively, bottom-up. + + :param the_roll: The expression to stringify. + :type the_roll: d20.Expression + :rtype: str + """ + return self._stringify(the_roll) + + def _stringify(self, node: ExpressionNode) -> str: + """ + Called on each node that needs to be stringified. + + :param node: The node to stringify. + :type node: d20.Number + :rtype: str + """ + handler = self._nodes[type(node)] + inside = handler(node) + if node.annotation: + return f"{inside} {node.annotation}" + return inside + + def _str_expression(self, node: Expression) -> str: + """ + :param node: The node to stringify. + :type node: d20.Expression + :rtype: str + """ + raise NotImplementedError + + def _str_literal(self, node: Literal) -> str: + """ + :param node: The node to stringify. + :type node: d20.Literal + :rtype: str + """ + raise NotImplementedError + + def _str_unop(self, node: UnOp) -> str: + """ + :param node: The node to stringify. + :type node: d20.UnOp + :rtype: str + """ + raise NotImplementedError + + def _str_binop(self, node: BinOp) -> str: + """ + :param node: The node to stringify. + :type node: d20.BinOp + :rtype: str + """ + raise NotImplementedError + + def _str_parenthetical(self, node: Parenthetical) -> str: + """ + :param node: The node to stringify. + :type node: d20.Parenthetical + :rtype: str + """ + raise NotImplementedError + + def _str_set(self, node: Set) -> str: + """ + :param node: The node to stringify. + :type node: d20.Set + :rtype: str + """ + raise NotImplementedError + + def _str_dice(self, node: Dice) -> str: + """ + :param node: The node to stringify. + :type node: d20.Dice + :rtype: str + """ + raise NotImplementedError + + def _str_die(self, node: Die) -> str: + """ + :param node: The node to stringify. + :type node: d20.Die + :rtype: str + """ + raise NotImplementedError + + @staticmethod + def _str_ops(operations: Iterable[SetOperator]) -> str: + return ''.join([str(op) for op in operations]) + + +class SimpleStringifier(Stringifier): + """ + Example stringifier. + """ + + def _str_expression(self, node): + return f"{self._stringify(node.roll)} = {int(node.total)}" + + def _str_literal(self, node): + history = ' -> '.join(map(str, node.values)) + if node.exploded: + return f"{history}!" + return history + + def _str_unop(self, node): + return f"{node.op}{self._stringify(node.value)}" + + def _str_binop(self, node): + return f"{self._stringify(node.left)} {node.op} {self._stringify(node.right)}" + + def _str_parenthetical(self, node): + return f"({self._stringify(node.value)}){self._str_ops(node.operations)}" + + def _str_set(self, node): + out = f"{', '.join([self._stringify(v) for v in node.values])}" + if len(node.values) == 1: + return f"({out},){self._str_ops(node.operations)}" + return f"({out}){self._str_ops(node.operations)}" + + def _str_dice(self, node): + the_dice = [self._stringify(die) for die in node.values] + return f"{node.num}d{node.size}{self._str_ops(node.operations)} ({', '.join(the_dice)})" + + def _str_die(self, node): + the_rolls = [self._stringify(val) for val in node.values] + return ', '.join(the_rolls) + + +class MarkdownStringifier(SimpleStringifier): + """ + Transforms roll expressions into Markdown. + """ + + class _MDContext: + def __init__(self): + self.in_dropped = False + + def reset(self): + self.in_dropped = False + + def __init__(self): + super().__init__() + self._context = self._MDContext() + + def stringify(self, the_roll): + self._context.reset() + return super().stringify(the_roll) + + def _stringify(self, node): + if not node.kept and not self._context.in_dropped: + self._context.in_dropped = True + inside = super()._stringify(node) + self._context.in_dropped = False + return f"~~{inside}~~" + return super()._stringify(node) + + def _str_expression(self, node): + return f"{self._stringify(node.roll)} = `{int(node.total)}`" + + def _str_die(self, node): + the_rolls = [] + for val in node.values: + inside = self._stringify(val) + if val.number == 1 or val.number == node.size: + inside = f"**{inside}**" + the_rolls.append(inside) + return ', '.join(the_rolls) diff --git a/modules/OneRoll/utils.py b/modules/OneRoll/utils.py new file mode 100644 index 0000000..3d300b0 --- /dev/null +++ b/modules/OneRoll/utils.py @@ -0,0 +1,221 @@ +import copy +from typing import Callable, Optional, TypeVar + +import oneroll # this import is here for the doctests +from oneroll import diceast, expression +from .dice import AdvType + +TreeType = TypeVar('TreeType', bound=diceast.ChildMixin) +ASTNode = TypeVar('ASTNode', bound=diceast.Node) +ExpressionNode = TypeVar('ExpressionNode', bound=expression.Number) + + +def ast_adv_copy(ast: ASTNode, advtype: AdvType) -> ASTNode: + """ + Returns a minimally shallow copy of a dice AST with respect to advantage. + + >>> tree = d20.parse("1d20 + 5") + >>> str(tree) + '1d20 + 5' + >>> str(ast_adv_copy(tree, d20.AdvType.ADV)) + '2d20kh1 + 5' + + :param d20.ast.Node ast: The parsed AST. + :param AdvType advtype: The advantage type to roll at. + :returns: The copied AST. + :rtype: d20.ast.Node + """ + root = copy.copy(ast) + if not advtype: + return root + + # find the leftmost node, making shallow copies all the way down + parent = child = root + while child.children: + parent = child + parent.left = child = copy.copy(parent.left) + + # is it dice? + if not isinstance(child, diceast.Dice): + return root + + # is it 1d20? + if not (child.num == 1 and child.size == 20): + return root + + # does it already have operations? + if not isinstance(parent, diceast.OperatedDice): + new_parent = diceast.OperatedDice(child) + parent.left = new_parent + parent = new_parent + else: + parent.operations = parent.operations.copy() + + # make the child 2d20 + child.num = 2 + + # add the kh1 operator + if advtype == 1: + high_or_low = diceast.SetSelector('h', 1) + else: + high_or_low = diceast.SetSelector('l', 1) + kh1 = diceast.SetOperator('k', [high_or_low]) + parent.operations.insert(0, kh1) + + return root + + +def simplify_expr_annotations(expr: ExpressionNode, ambig_inherit: Optional[str] = None): + """ + Transforms an expression in place by simplifying the annotations using a bubble-up method. + + >>> roll_expr = d20.roll("1d20[foo]+3").expr + >>> simplify_expr_annotations(roll_expr.roll) + >>> d20.SimpleStringifier().stringify(roll_expr) + "1d20 (4) + 3 [foo] = 7" + + :param d20.Number expr: The expression to transform. + :param ambig_inherit: When encountering a child node with no annotation and the parent has ambiguous types, which + to inherit. Can be ``None`` for no inherit, ``'left'`` for leftmost, or ``'right'`` for rightmost. + :type ambig_inherit: Optional[str] + """ + if ambig_inherit not in ('left', 'right', None): + raise ValueError("ambig_inherit must be 'left', 'right', or None.") + + def do_simplify(node): + possible_types = [] + child_possibilities = {} + for child in node.children: + child_possibilities[child] = do_simplify(child) + possible_types.extend(t for t in child_possibilities[child] if t not in possible_types) + if node.annotation is not None: + possible_types.append(node.annotation) + + # if I have no type or the same as children and all my children have the same type, inherit + if len(possible_types) == 1: + node.annotation = possible_types[0] + for child in node.children: + child.annotation = None + # if there are ambiguous types, resolve children by ambiguity rules + # unless it would change the right side of a multiplicative binop + elif len(possible_types) and ambig_inherit is not None: + for i, child in enumerate(node.children): + if child_possibilities[child]: # if the child already provides an annotation or ambiguity + continue + elif isinstance(node, expression.BinOp) \ + and node.op in {'*', '/', '//', '%'} \ + and i: # if the child is the right side of a multiplicative binop + continue + elif ambig_inherit == 'left': + child.annotation = possible_types[0] + elif ambig_inherit == 'right': + child.annotation = possible_types[-1] + + # return all possible types + return tuple(possible_types) + + do_simplify(expr) + + +def simplify_expr(expr: expression.Expression, **kwargs): + """ + Transforms an expression in place by simplifying it (removing all dice and evaluating branches with respect to + annotations). + + >>> roll_expr = d20.roll("1d20[foo] + 3 - 1d4[bar]").expr + >>> simplify_expr(roll_expr) + >>> d20.SimpleStringifier().stringify(roll_expr) + "7 [foo] - 2 [bar] = 5" + + :param d20.Expression expr: The expression to transform. + :param kwargs: Arguments that are passed to :func:`simplify_expr_annotations`. + """ + simplify_expr_annotations(expr.roll, **kwargs) + + def do_simplify(node, first=False): + """returns a pair of (replacement, branch had replacement)""" + if node.annotation: + return expression.Literal(node.total, annotation=node.annotation), True + + # pass 1: recursively replace branches with annotations, marking which branches had replacements + had_replacement = set() + for i, child in enumerate(node.children): + replacement, branch_had = do_simplify(child) + if branch_had: + had_replacement.add(i) + if replacement is not child: + node.set_child(i, replacement) + + # pass 2: replace no-annotation branches + for i, child in enumerate(node.children): + if (i not in had_replacement) and (had_replacement or first): + # here is the furthest we can bubble up a no-annotation branch + replacement = expression.Literal(child.total) + node.set_child(i, replacement) + + return node, bool(had_replacement) + + do_simplify(expr, True) + + +def tree_map(func: Callable[[TreeType], TreeType], node: TreeType) -> TreeType: + """ + Returns a copy of the tree, with each node replaced with ``func(node)``. + + :param func: A transformer function. + :type func: Callable[[d20.ast.ChildMixin], d20.ast.ChildMixin] + :param node: The root of the tree to transform. + :type node: d20.ast.ChildMixin + :rtype: d20.ast.ChildMixin + """ + copied = copy.copy(node) + for i, child in enumerate(copied.children): + copied.set_child(i, tree_map(func, child)) + return func(copied) + + +def leftmost(root: TreeType) -> TreeType: + """ + Returns the leftmost leaf in this tree. + + :param d20.ast.ChildMixin root: The root node of the tree. + :rtype: d20.ast.ChildMixin + """ + left = root + while left.children: + left = left.children[0] + return left + + +def rightmost(root: TreeType) -> TreeType: + """ + Returns the rightmost leaf in this tree. + + :param d20.ast.ChildMixin root: The root node of the tree. + :rtype: d20.ast.ChildMixin + """ + right = root + while right.children: + right = right.children[-1] + return right + + +def dfs(node: TreeType, predicate: Callable[[TreeType], bool]) -> Optional[TreeType]: + """ + Returns the first node in the tree such that ``predicate(node)`` is True, searching depth-first left-to-right. + Returns None if no node satisfying the predicate was found. + + :param d20.ast.ChildMixin node: The root node of the tree. + :param predicate: A predicate function. + :type predicate: Callable[[d20.ast.ChildMixin], bool] + :rtype: Optional[d20.ast.ChildMixin] + """ + if predicate(node): + return node + + for child in node.children: + result = dfs(child, predicate) + if result is not None: + return result + + return None diff --git a/modules/TRPGNivisSDK/Grammar/Token b/modules/TRPGNivisSDK/Grammar/Token new file mode 100644 index 0000000..0de3014 --- /dev/null +++ b/modules/TRPGNivisSDK/Grammar/Token @@ -0,0 +1,55 @@ +LPAR '(' +RPAR ')' +LSQB '[' +RSQB ']' +COLON ':' +COMMA ',' +SEMI ';' +PLUS '+' +MINUS '-' +STAR '*' +SLASH '/' +VBAR '|' +AMPER '&' +LESS '<' +GREATER '>' +EQUAL '=' +DOT '.' +PERCENT '%' +LBRACE '{' +RBRACE '}' +EQEQUAL '==' +NOTEQUAL '!=' +LESSEQUAL '<=' +GREATEREQUAL '>=' +TILDE '~' +CIRCUMFLEX '^' +LEFTSHIFT '<<' +RIGHTSHIFT '>>' +DOUBLESTAR '**' +PLUSEQUAL '+=' +MINEQUAL '-=' +STAREQUAL '*=' +SLASHEQUAL '/=' +PERCENTEQUAL '%=' +AMPEREQUAL '&=' +VBAREQUAL '|=' +CIRCUMFLEXEQUAL '^=' +LEFTSHIFTEQUAL '<<=' +RIGHTSHIFTEQUAL '>>=' +DOUBLESTAREQUAL '**=' +DOUBLESLASH '//' +DOUBLESLASHEQUAL '//=' +AT '@' +ATEQUAL '@=' +RARROW '->' +ELLIPSIS '...' +COLONEQUAL ':=' +EXCLAMATION '!' +INTEGER 'INTEGER' +EOF 'EOF' +SPACE ' ' + + +AWAIT +ASYNC diff --git a/modules/TRPGNivisSDK/Lib/IOStream/__init__.nivis b/modules/TRPGNivisSDK/Lib/IOStream/__init__.nivis new file mode 100644 index 0000000..d38024d --- /dev/null +++ b/modules/TRPGNivisSDK/Lib/IOStream/__init__.nivis @@ -0,0 +1 @@ +# TODO: nivis Plugins in VsCode \ No newline at end of file diff --git a/modules/TRPGNivisSDK/Modules/asyncio/__init__.py b/modules/TRPGNivisSDK/Modules/asyncio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/TRPGNivisSDK/__init__.py b/modules/TRPGNivisSDK/__init__.py new file mode 100644 index 0000000..943368b --- /dev/null +++ b/modules/TRPGNivisSDK/__init__.py @@ -0,0 +1,12 @@ +"""nivis + +@TODO Lexer support +@BODY Lex function. +""" + +__all__ = ["Execution", "Interpreter", "Lexer", "Parser"] + +from .execution import Execution +from .interpreter import Interpreter +from .lexer import Lexer +from .parsers import Parser diff --git a/modules/TRPGNivisSDK/exception.py b/modules/TRPGNivisSDK/exception.py new file mode 100644 index 0000000..0ec22e5 --- /dev/null +++ b/modules/TRPGNivisSDK/exception.py @@ -0,0 +1,37 @@ +class PsiException(Exception): + """ + An exception class for Psi-specific exceptions. + + This class inherits from the built-in `Exception` class. + + Example: + ```python + raise PsiException("An error occurred in the Psi code.") + ``` + """ + + +class ValueError(PsiException): + """ + An exception class for value-related errors in Psi code. + + This class inherits from the `PsiException` class. + + Example: + ```python + raise ValueError("Invalid value encountered in the Psi code.") + ``` + """ + + +class GrammarError(PsiException): + """ + An exception class for grammar-related errors in Psi code. + + This class inherits from the `PsiException` class. + + Example: + ```python + raise GrammarError("Invalid grammar encountered in the Psi code.") + ``` + """ diff --git a/modules/TRPGNivisSDK/execution.py b/modules/TRPGNivisSDK/execution.py new file mode 100644 index 0000000..09055cf --- /dev/null +++ b/modules/TRPGNivisSDK/execution.py @@ -0,0 +1,47 @@ +from .parsers import Parser +from .interpreter import Interpreter + +__all__ = ["Execution"] + + +class Execution: + """ + A class representing the execution of Psi code. + + Args: + input: The input code to be executed. + + Returns: + None + + Example: + ```python + execution = Execution("print('Hello, World!')") + execution.execute() + ``` + """ + + def __init__(self, input): + """ + Initializes an Execution object. + + Args: + input: The input code to be executed. + + Returns: + None + """ + self.input = input + + def execute(self): + """ + Executes the input code. + + Returns: + The result of the execution. + """ + parser = Parser(self.input) + ast = parser.parse() + + interpreter = Interpreter(ast) + return interpreter.interpret() diff --git a/modules/TRPGNivisSDK/interpreter.py b/modules/TRPGNivisSDK/interpreter.py new file mode 100644 index 0000000..6322180 --- /dev/null +++ b/modules/TRPGNivisSDK/interpreter.py @@ -0,0 +1,76 @@ +from .lexer import Token + + +__all__ = ["Interpreter"] + + +class Interpreter: + """ + A class representing an interpreter for Psi code. + + Args: + ast: The abstract syntax tree (AST) of the code to be interpreted. + + Returns: + None + + Example: + ```python + interpreter = Interpreter(ast) + interpreter.interpret() + ``` + """ + + def __init__(self, ast): + """ + Initializes an Interpreter object. + + Args: + ast: The abstract syntax tree (AST) of the code to be interpreted. + + Returns: + None + """ + self.ast = ast + + def interpret(self): + """ + Interprets the code represented by the AST. + + Returns: + The result of the interpretation. + """ + return self.interpret_expr(self.ast) + + def interpret_expr(self, node): + """ + Interprets an expression node in the AST. + + Args: + node: The expression node to be interpreted. + + Returns: + The result of the interpretation. + """ + if isinstance(node, Token): + return node.value + elif isinstance(node, list): + for expr in node: + result = self.interpret_expr(expr) + if result is not None: + return result + + def interpret_condition(self, node): + """ + Interprets a condition node in the AST. + + Args: + node: The condition node to be interpreted. + + Returns: + The result of the interpretation. + """ + variable = self.interpret_expr(node[0]) + value = self.interpret_expr(node[2]) + + return variable == value diff --git a/modules/TRPGNivisSDK/lexer.py b/modules/TRPGNivisSDK/lexer.py new file mode 100644 index 0000000..7ba94e3 --- /dev/null +++ b/modules/TRPGNivisSDK/lexer.py @@ -0,0 +1,276 @@ +""" +Token and Lexer Documentation +============================= + +This module provides the `Token` and `Lexer` classes for tokenizing input strings. + +Token Class +----------- + +The `Token` class represents a token with a type, value, and position in the input string. It is a subclass of the built-in `dict` class. + +Attributes: +- `type` (str): The type of the token. +- `value` (str or int): The value of the token. +- `position` (int): The position of the token in the input string. + +Methods: +- `__getattr__(self, name)`: Retrieves the value of an attribute by name. Raises an `AttributeError` if the attribute does not exist. + +Lexer Class +----------- + +The `Lexer` class tokenizes an input string using a set of rules. + +Attributes: +- `input` (str): The input string to tokenize. +- `position` (int): The current position in the input string. +- `tokens` (list): The list of tokens generated by the lexer. + +Methods: +- `get_next_token(self)`: Retrieves the next token from the input string. +- `__iter__(self)`: Returns an iterator over the tokens. +- `__getitem__(self, index)`: Retrieves a token by index. +- `__len__(self)`: Returns the number of tokens. + +Usage Example +------------- + +```python +lexer = Lexer(''' +@newMessage: { + ? message == 1: reply: hi + ! reply: no +} +''') + +token = lexer.get_next_token() +while token['type'] != 'EOF': + print(f'Type: {token["type"]}, Value: {token["value"]}, Position: {token["position"]}') + token = lexer.get_next_token() + +print("\nAll tokens:") +print([t['type'] for t in lexer]) +""" +from .exception import ValueError + +__all__ = ["Token", "Lexer"] + + +class Token(dict): + """ + A class representing a token in the lexer. + + Args: + type: The type of the token. + value: The value of the token. + position: The position of the token. + + Returns: + None + + Example: + ```python + token = Token("identifier", "x", (1, 5)) + ``` + """ + + def __init__(self, type, value, position): + """ + Initializes a Token object. + + Args: + type: The type of the token. + value: The value of the token. + position: The position of the token. + + Returns: + None + """ + super().__init__(type=type, value=value, position=position) + + def __getattr__(self, name): + """ + Retrieves the value of an attribute from the Token object. + + Args: + name: The name of the attribute. + + Returns: + The value of the attribute. + + Raises: + AttributeError: Raised when the attribute does not exist. + """ + try: + return self[name] + except KeyError as e: + raise AttributeError(f"'Token' object has no attribute '{name}'") from e + + +class Lexer: + """ + A class representing a lexer for Psi code. + + Args: + input: The input code to be lexed. + + Returns: + None + + Example: + ```python + lexer = Lexer("x = 10") + for token in lexer: + print(token) + ``` + """ + + def __init__(self, input): + """ + Initializes a Lexer object. + + Args: + input: The input code to be lexed. + + Returns: + None + """ + self.input = input + self.position = 0 + self.tokens = [] + + def get_next_token(self): + """ + Retrieves the next token from the input code. + + Returns: + The next token. + + Raises: + Exception: Raised when an unknown character is encountered. + """ + while self.position < len(self.input): + current_char = self.input[self.position] + + if current_char.isspace(): + self.position += 1 + continue + + if current_char == "#": + self.position += 1 + while ( + self.position < len(self.input) + and self.input[self.position] != "\n" + ): + self.position += 1 + continue + + if ( + current_char == "/" + and self.position + 1 < len(self.input) + and self.input[self.position + 1] == "*" + ): + self.position += 2 + while self.position < len(self.input) - 1 and ( + self.input[self.position] != "*" + or self.input[self.position + 1] != "/" + ): + self.position += 1 + if self.position < len(self.input) - 1: + self.position += 2 + continue + + if current_char.isalpha(): + start_position = self.position + while ( + self.position < len(self.input) + and self.input[self.position].isalnum() + ): + self.position += 1 + token = Token( + "IDENTIFIER", + self.input[start_position : self.position], + start_position, + ) + self.tokens.append(token) + return token + + if current_char.isdigit(): + start_position = self.position + while ( + self.position < len(self.input) + and self.input[self.position].isdigit() + ): + self.position += 1 + token = Token( + "INTEGER", + int(self.input[start_position : self.position]), + start_position, + ) + self.tokens.append(token) + return token + + if current_char in {"<", ">", "=", "!", "&", "|", "@"}: + if self.position + 1 < len(self.input) and self.input[ + self.position + 1 + ] in {"=", "&", "|"}: + token = Token( + "OPERATOR", + current_char + self.input[self.position + 1], + self.position, + ) + self.position += 2 + else: + token = Token("OPERATOR", current_char, self.position) + self.position += 1 + self.tokens.append(token) + return token + + if current_char in {"{", "}", "(", ")", "[", "]", ";", ",", ".", ":"}: + return self._extracted_from_get_next_token_64("SEPARATOR", current_char) + if current_char in {"?", "!", "|"}: + return self._extracted_from_get_next_token_64("CONTROL", current_char) + self.position += 1 + raise ValueError(f"Unknown character: {current_char}") + + token = Token("EOF", None, self.position) + self.tokens.append(token) + return token + + # TODO Rename this here and in `get_next_token` + def _extracted_from_get_next_token_64(self, arg0, current_char): + token = Token(arg0, current_char, self.position) + self.position += 1 + self.tokens.append(token) + return token + + def __iter__(self): + """ + Returns an iterator over the tokens. + + Returns: + An iterator over the tokens. + """ + return iter(self.tokens) + + def __getitem__(self, index): + """ + Retrieves the token at the specified index. + + Args: + index: The index of the token. + + Returns: + The token at the specified index. + """ + return self.tokens[index] + + def __len__(self): + """ + Returns the number of tokens. + + Returns: + The number of tokens. + """ + return len(self.tokens) diff --git a/modules/TRPGNivisSDK/mathmatics.py b/modules/TRPGNivisSDK/mathmatics.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/TRPGNivisSDK/parsers.py b/modules/TRPGNivisSDK/parsers.py new file mode 100644 index 0000000..ca004f7 --- /dev/null +++ b/modules/TRPGNivisSDK/parsers.py @@ -0,0 +1,145 @@ +from .lexer import Lexer, Token + + +__all__ = ["Parser"] + + +class Parser: + """ + A class representing a parser for Psi code. + + Args: + input: The input code to be parsed. + + Returns: + None + + Example: + ```python + parser = Parser(input) + parser.parse() + ``` + """ + + def __init__(self, input): + """ + Initializes a Parser object. + + Args: + input: The input code to be parsed. + + Returns: + None + """ + self.lexer = Lexer(input) + self.tokens = iter(self.lexer) + self.current_token = next(self.tokens) + + def parse(self): + """ + Parses the input code. + + Returns: + The result of the parsing. + """ + return self.parse_expr() + + def parse_expr(self): + """ + Parses an expression in the input code. + + Returns: + The result of the parsing. + """ + token = self.current_token + if token.value == "?": + self.eat("?") + + condition = self.parse_condition() + + self.eat(":") + + if condition: + result = self.parse_reply() + else: + result = None + + return result + + def parse_condition(self): + """ + Parses a condition in the input code. + + Returns: + The result of the parsing. + """ + variable = self.parse_variable() + self.eat("==") + value = self.parse_value() + + return variable == value + + def parse_variable(self): + """ + Parses a variable in the input code. + + Returns: + The result of the parsing. + """ + token = self.current_token + self.eat("IDENTIFIER") + return token.value + + def parse_value(self): + """ + Parses a value in the input code. + + Returns: + The result of the parsing. + + Raises: + Exception: Raised when an invalid value is encountered. + """ + token = self.current_token + if token.type == "INTEGER": + self.eat("INTEGER") + return token.value + else: + raise Exception(f"Invalid value: {token.value}") + + def parse_reply(self): + """ + Parses a reply in the input code. + + Returns: + The result of the parsing. + + Raises: + Exception: Raised when an invalid reply is encountered. + """ + self.eat("reply") + self.eat(":") + + token = self.current_token + if token.type != "SEPARATOR": + raise Exception(f"Invalid reply: {token.value}") + + return token.value + + def eat(self, expected_type): + """ + Consumes the current token if it matches the expected type. + + Args: + expected_type: The expected type of the token. + + Returns: + None + + Raises: + Exception: Raised when an unexpected token is encountered. + """ + if self.current_token.type == expected_type: + self.current_token = next(self.tokens) + else: + raise Exception(f"Unexpected token: {self.current_token.value}") diff --git a/modules/TRPGNivisSDK/type.py b/modules/TRPGNivisSDK/type.py new file mode 100644 index 0000000..e69de29