From 74c7c1cff77b2894682d61aea3a518d8d73c51aa Mon Sep 17 00:00:00 2001 From: haliphax Date: Thu, 28 Aug 2025 10:15:31 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 61 ---------------- package.json | 1 - realm_api/rpc/__init__.py | 8 +-- realm_api/rpc/roll.py | 7 -- realm_api/rpc/roll/__init__.py | 127 +++++++++++++++++++++++++++++++++ realm_api/rpc/roll/parse.py | 86 ++++++++++++++++++++++ 6 files changed, 217 insertions(+), 73 deletions(-) delete mode 100644 realm_api/rpc/roll.py create mode 100644 realm_api/rpc/roll/__init__.py create mode 100644 realm_api/rpc/roll/parse.py diff --git a/package-lock.json b/package-lock.json index 6eefa1c..66907bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.1", "commitlint": "^17.7.1", - "commitlint-config-gitmoji": "^2.3.1", "gitmoji-cli": "^8.5.0", "husky": "^9.1.7", "nano-staged": "^0.8.0", @@ -774,41 +773,6 @@ "node": ">=12" } }, - "node_modules/@gitmoji/commit-types": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@gitmoji/commit-types/-/commit-types-1.1.5.tgz", - "integrity": "sha512-8D3FZMRY+gtYpTcHG1SOGmm9CFqxNh6rI9xDoCydxHxnWgqInbdF3nk9gibW5gXA58Hf2cVcJaLEcGOKLRAtmw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@gitmoji/gitmoji-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@gitmoji/gitmoji-regex/-/gitmoji-regex-1.0.0.tgz", - "integrity": "sha512-+BFXxcWCxn0UIYuG1v5n9SfaCCS8tw95j1x3QsTJRdGGiihRyVLTHiu1wrHlzH3z4nYXKjCKiZFTdWwMjRI+gQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "emoji-regex": "^10", - "gitmojis": "^3" - } - }, - "node_modules/@gitmoji/gitmoji-regex/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@gitmoji/parser-opts": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@gitmoji/parser-opts/-/parser-opts-1.4.0.tgz", - "integrity": "sha512-zzmx/vtpdB/ijjUm7u9OzHNCXWKpSbzVEgVzOzhilMgoTBlUDyInZFUtiCTV+Wf4oCP9nxGa/kQGQFfN+XLH1g==", - "dev": true, - "license": "ISC", - "dependencies": { - "@gitmoji/gitmoji-regex": "1.0.0" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -2603,31 +2567,6 @@ "node": ">=v14" } }, - "node_modules/commitlint-config-gitmoji": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/commitlint-config-gitmoji/-/commitlint-config-gitmoji-2.3.1.tgz", - "integrity": "sha512-T15ssbsyNc6szHlnGWo0/xvIA1mObqM70E9TwKNVTpksxhm+OdFht8hvDdKJAVi4nlZX5tcfTeILOi7SHBGH3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^17", - "@gitmoji/commit-types": "1.1.5", - "@gitmoji/parser-opts": "1.4.0", - "commitlint-plugin-gitmoji": "2.2.6" - } - }, - "node_modules/commitlint-plugin-gitmoji": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/commitlint-plugin-gitmoji/-/commitlint-plugin-gitmoji-2.2.6.tgz", - "integrity": "sha512-oKHPHeNXby0Ix0ZbHVSK5ZyPx1V4fyBjLOy93cYwXhOEPXe36nkDc/HDPFfQpzx1vz39277TaP9LScbqTbscfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@commitlint/types": "^17", - "@gitmoji/gitmoji-regex": "1.0.0", - "gitmojis": "^3" - } - }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", diff --git a/package.json b/package.json index e05a9f9..312c18f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.1", "commitlint": "^17.7.1", - "commitlint-config-gitmoji": "^2.3.1", "gitmoji-cli": "^8.5.0", "husky": "^9.1.7", "nano-staged": "^0.8.0", diff --git a/realm_api/rpc/__init__.py b/realm_api/rpc/__init__.py index 1f6d046..f835eaa 100644 --- a/realm_api/rpc/__init__.py +++ b/realm_api/rpc/__init__.py @@ -26,21 +26,21 @@ async def init_pubsub() -> aio.Task: async def shutdown_pubsub(task: aio.Task): - pubsub.unsubscribe() + await pubsub.unsubscribe() await pubsub.close() task.cancel() -def handler(message: dict): +async def handler(message: dict): """Handle an incoming RPC operation and publish the result.""" data: dict = json.loads(message["data"]) logger.info(f"RPC op: {data['op']}") - result = handlers[data["op"]]( + result = await handlers[data["op"]]( *data.get("args", []), **data.get("kwargs", dict()), ) - redis_conn.publish(data["uuid"], json.dumps(result)) + await redis_conn.publish(data["uuid"], json.dumps(result)) async def rpc_bot(op: str, *args, timeout=3, **kwargs): diff --git a/realm_api/rpc/roll.py b/realm_api/rpc/roll.py deleted file mode 100644 index 9a76ee2..0000000 --- a/realm_api/rpc/roll.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Dice roller RPC module""" - - -def roll_handler(data: dict): - """Roll them bones.""" - - return 0 diff --git a/realm_api/rpc/roll/__init__.py b/realm_api/rpc/roll/__init__.py new file mode 100644 index 0000000..e8baac5 --- /dev/null +++ b/realm_api/rpc/roll/__init__.py @@ -0,0 +1,127 @@ +"""Dice roller RPC module""" + +# stdlib +from random import randint +from re import compile + +# api +from realm_schema import ( + ConstantModifier, + DiceRoll, + RollResults, + RollSegment, + SegmentResult, +) + +# local +from .parse import parse_segments + + +explode_regex = compile(r"!(?P\d*)") +"""Regex for exploding die extra""" + +keep_hilow_regex = compile(r"(?Pk[hl])(?P\d*)") +"""Regex for keep highest/lowest extras""" + + +def roll_segment(segment: RollSegment) -> SegmentResult: + """Calculate the results of an individual RollSegment instance.""" + + if isinstance(segment, ConstantModifier): + return SegmentResult( + segment=segment, + total=segment.number * (-1 if segment.negative else 1), + ) + + if isinstance(segment, DiceRoll): + limit = 0 + exploding = False + result = SegmentResult(segment=segment, rolls=[], work="") + assert result.rolls is not None + assert result.work is not None + + if segment.extra and segment.extra[0] == "!": + exploding = True + + # explosion limit + if len(segment.extra) > 1: + explode_limit_match = explode_regex.match(segment.extra) + assert explode_limit_match + limit = int(explode_limit_match.groupdict()["num"]) + + # roll dice + for _ in range(segment.dice): + roll = randint(1, segment.faces) + exploded = 0 + + # exploding die + while ( + exploding + and roll == segment.faces + and (limit == 0 or exploded < limit) + ): + exploded += 1 + result.rolls.append(roll) + result.total += roll + roll = randint(1, segment.faces) + + result.rolls.append(roll) + result.total += roll + + # keep methods + if segment.extra and segment.extra.startswith("k"): + ord_rolls = sorted(result.rolls) + args_match = keep_hilow_regex.match(segment.extra) + assert args_match + args = args_match.groupdict() + fun = args["fun"] + keep = int(args["num"]) if args["num"] else 1 + + assert keep < segment.dice and keep > 0 + drop = range(segment.dice - keep) + + # keep highest + if fun == "kh": + for i in drop: + result.total -= ord_rolls[i] + + # keep lowest + elif fun == "kl": + for i in drop: + result.total -= ord_rolls[-(i + 1)] + + rolls = [ + f"**{roll}**" if roll == segment.faces else f"{roll}" + for roll in result.rolls + ] + result.work = "".join( + [ + f"[{', '.join(rolls)}] = ", + f"**{'-' if segment.negative else ''}{result.total}**", + ] + ) + + if segment.negative: + result.total *= -1 + + return result + + raise NotImplementedError() + + +async def roll_handler(formula: str) -> list[RollResults]: + """Roll them bones.""" + + results = [] + + try: + batches = parse_segments(formula) + + for segments in batches: + results.append( + RollResults(results=[roll_segment(s) for s in segments]) + ) + except Exception: + return [] + + return results diff --git a/realm_api/rpc/roll/parse.py b/realm_api/rpc/roll/parse.py new file mode 100644 index 0000000..528b40f --- /dev/null +++ b/realm_api/rpc/roll/parse.py @@ -0,0 +1,86 @@ +"""Parse segments from roll command arguments""" + +# stdlib +from re import compile + +# local +from realm_schema import ConstantModifier, DiceRoll, RollSegment + +roll_regex = compile( + r"^(?P" + r"(?P\d*)" + r"d(?P\d+)" + r"(?P!\d*|k[hl]\d*)?" + r")" + r"(x(?P\d+))?" +) +"""Regex pattern for initial roll""" + +addl_roll_regex = compile( + r"(?P" + r"(?P[-+])" + r"(?P\d*)" + r"(d(?P\d+)(?P!\d*|k[hl]\d*)?)?" + r")" + r"(x(?P\d+))?" +) +"""Regex pattern for additional modifier rolls/constants""" + + +def parse_segments(roll: str) -> list[list[RollSegment]]: + """Parse a roll formula string into a list of RollSegment objects.""" + + segments = [] + + # strip all whitespace + roll = roll.replace(" ", "") + + # parse first roll + first = roll_regex.match(roll) + assert first + first = first.groupdict() + + # parse variable number of mods + mods = [r.groupdict() for r in addl_roll_regex.finditer(roll)] + + batch = int(first["batch"] or 1) + + for m in mods: + if m["batch"]: + batch = int(m["batch"]) + + batches = [] + + for _ in range(batch): + segments: list[RollSegment] = [ + DiceRoll( + raw=first["all"], + dice=int(first["num"] or "1"), + faces=int(first["die"]), + extra=first["fun"], + ) + ] + + for mod in mods: + if mod["die"]: + segments.append( + DiceRoll( + raw=mod["all"], + negative=mod["op"] == "-", + dice=int(mod["num"] or "1"), + faces=int(mod["die"]), + extra=mod["fun"], + ) + ) + else: + segments.append( + ConstantModifier( + raw=mod["all"], + negative=mod["op"] == "-", + number=int(mod["num"]), + ) + ) + + batches.append(segments) + + return batches From 2e75020506ae46f9ad03670d448c429d1cdfe262 Mon Sep 17 00:00:00 2001 From: haliphax Date: Thu, 28 Aug 2025 21:15:52 -0500 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20move=20roll=20logic=20from=20bo?= =?UTF-8?q?t=20to=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- realm_api/rpc/__init__.py | 6 ++++-- realm_api/rpc/roll/__init__.py | 13 ++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5e98856..ec9334b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ COPY realm_api /app/realm_api RUN <<-EOF mkdir -p /app/data - pip install --no-cache -Ue . + pip install --no-cache --no-warn-script-location -Ue . EOF ENTRYPOINT [ "/usr/local/bin/python", "-m", "realm_api" ] diff --git a/realm_api/rpc/__init__.py b/realm_api/rpc/__init__.py index f835eaa..b18469c 100644 --- a/realm_api/rpc/__init__.py +++ b/realm_api/rpc/__init__.py @@ -7,6 +7,7 @@ from uuid import uuid4 # 3rd party +from pydantic import BaseModel from redis.asyncio import StrictRedis # local @@ -36,11 +37,12 @@ async def handler(message: dict): data: dict = json.loads(message["data"]) logger.info(f"RPC op: {data['op']}") - result = await handlers[data["op"]]( + result: BaseModel = await handlers[data["op"]]( *data.get("args", []), **data.get("kwargs", dict()), ) - await redis_conn.publish(data["uuid"], json.dumps(result)) + response = result.model_dump_json() + await redis_conn.publish(data["uuid"], response) async def rpc_bot(op: str, *args, timeout=3, **kwargs): diff --git a/realm_api/rpc/roll/__init__.py b/realm_api/rpc/roll/__init__.py index e8baac5..02a2ab0 100644 --- a/realm_api/rpc/roll/__init__.py +++ b/realm_api/rpc/roll/__init__.py @@ -6,6 +6,7 @@ # api from realm_schema import ( + BatchResults, ConstantModifier, DiceRoll, RollResults, @@ -14,6 +15,7 @@ ) # local +from realm_api.logging import logger from .parse import parse_segments @@ -109,19 +111,20 @@ def roll_segment(segment: RollSegment) -> SegmentResult: raise NotImplementedError() -async def roll_handler(formula: str) -> list[RollResults]: +async def roll_handler(formula: str) -> BatchResults: """Roll them bones.""" - results = [] + results = BatchResults(results=[]) try: batches = parse_segments(formula) for segments in batches: - results.append( + results.results.append( RollResults(results=[roll_segment(s) for s in segments]) ) - except Exception: - return [] + except Exception as ex: + logger.warning(ex) + return BatchResults(results=[]) return results