267 lines
7.4 KiB
Python
267 lines
7.4 KiB
Python
import random
|
|
from typing import Final
|
|
|
|
from tests.rewrite.models import (
|
|
ContextInput,
|
|
Countdown,
|
|
Counters,
|
|
CurrentPools,
|
|
CurrentRoll,
|
|
Entity,
|
|
Input,
|
|
PoolByCategory,
|
|
Rates,
|
|
Storage,
|
|
how_do_i_explain_this,
|
|
)
|
|
from wf_authoring import NodeReturn, node
|
|
from wf_authoring.nodes.result import NoOutput, Nothing, outcome
|
|
from wf_core.tokens import END
|
|
|
|
# to the functions
|
|
# @node(outcomes=("ok", "end")) # breaks because of input | nothing
|
|
|
|
|
|
@node
|
|
def init(
|
|
_inp: Input,
|
|
# ) -> NodeReturn[Input | Nothing]: # JUST doesnt work if the types are above
|
|
) -> None:
|
|
# why cant use basemodel? should we convert typeddicts to basemodels? Why cant use
|
|
"""i hope that this is done by default, because langgraph DOESNT. why.
|
|
|
|
since input and state has the same keys
|
|
"""
|
|
# if ctx.context["type"] == 'normal' and ctx.context["initial_rates"]["r_240"] != 0:
|
|
# "what now" # pylint: disable=W0105
|
|
# return (
|
|
# NodeReturn("ok", inp)
|
|
# if inp.countdown > 0
|
|
# else NodeReturn("end", Nothing()) # end early
|
|
# )
|
|
# if inp.pity_120_available and inp.context["type"] == "normal":
|
|
# "i doesnt care"
|
|
# return Nothing()
|
|
# return None
|
|
# return inp
|
|
|
|
|
|
@node(outcomes=("0", "65"))
|
|
def rate_booster(c: Counters) -> NoOutput:
|
|
"""This was an edge.
|
|
|
|
Should I implement r65 here too... i think not.
|
|
|
|
the flow was init --rate_booster-> (65, r65), (0, r0), which outputs to the same
|
|
rate_guarantee, which is no op.
|
|
"""
|
|
if c.counter["c_80"] >= 65:
|
|
return outcome("65")
|
|
return outcome("0")
|
|
|
|
|
|
def _popped(storage: list[Entity]) -> bool:
|
|
return any(c["category"] == "240" for c in reversed(storage))
|
|
|
|
|
|
@node
|
|
def popped(s: Storage) -> how_do_i_explain_this:
|
|
return how_do_i_explain_this(pity_120_available=not _popped(s.storage))
|
|
|
|
|
|
# OHH so each of these MAY only only use whatever is needed? woah. i like
|
|
|
|
|
|
class CountersContext(Counters, ContextInput): ...
|
|
|
|
|
|
# this gets annoying.
|
|
class CountersContextOutputInputAhhModelType(
|
|
CountersContext, Storage, how_do_i_explain_this
|
|
): ...
|
|
|
|
|
|
@node(outcomes=("240", "80", "10", "1"))
|
|
def pre_roll_router(c: CountersContextOutputInputAhhModelType) -> NoOutput:
|
|
"""another edge.
|
|
|
|
the conditional router calculates which to reset to 0 (guarantee the rest)
|
|
|
|
the flow was rate_guarantee --router-> (1 -> prep), (10 -.-> RateChange.r10 --> prep) (80 -.-> r80 --> prep), (240 -.-> r240 -> prep).
|
|
"""
|
|
sc, ct = c.simple_counter, c.counter
|
|
if c.context["type"] == "banner" and (
|
|
(sc == 120 and c.pity_120_available) or (sc > 0 and sc % 240 == 0)
|
|
):
|
|
return outcome("240")
|
|
if ct.get("c_80", 0) % 80 == 0:
|
|
return outcome("80")
|
|
if ct.get("c_10", 0) % 10 == 0:
|
|
return outcome("10")
|
|
return outcome("1")
|
|
|
|
|
|
# now to the weeds of it. a Class!
|
|
|
|
|
|
class RateChange:
|
|
@node(name="force 6* rating")
|
|
@staticmethod
|
|
def r80(r: Rates) -> Rates:
|
|
return Rates.model_validate(
|
|
{
|
|
"rates": {
|
|
"r_1": 0,
|
|
"r_10": 0,
|
|
"r_80": r.rates["r_80"],
|
|
"r_240": r.rates["r_240"],
|
|
}
|
|
}
|
|
)
|
|
|
|
@node(name="force banner rating")
|
|
@staticmethod
|
|
def r240(_: Nothing) -> Rates:
|
|
return Rates.model_validate(
|
|
{"rates": {"r_1": 0, "r_10": 0, "r_80": 0, "r_240": 1}}
|
|
)
|
|
|
|
@node(name="force 5*+ rating")
|
|
@staticmethod
|
|
def r10(r: Rates) -> Rates:
|
|
return Rates.model_validate(
|
|
{
|
|
"rates": {
|
|
"r_1": 0,
|
|
"r_10": r.rates["r_10"],
|
|
"r_80": r.rates["r_80"],
|
|
"r_240": r.rates["r_240"],
|
|
}
|
|
}
|
|
)
|
|
|
|
@node(name="buff 6* rating")
|
|
@staticmethod
|
|
def r65(state: CountersContext) -> Rates:
|
|
c = state.counter
|
|
assert c["c_80"] >= 65, f"routed wrongly, 80 pity currently at {c['c_80']}"
|
|
n = c["c_80"] - 64
|
|
rpn = n * 0.05
|
|
br = state.context["initial_rates"]
|
|
if state.context["type"] == "banner":
|
|
r240 = br["r_240"] * (1 + rpn / 2)
|
|
r80 = br["r_80"] * (1 + rpn / 2)
|
|
else:
|
|
r240 = 0
|
|
r80 = br["r_80"] * (1 + rpn)
|
|
r10 = br["r_10"] # use initial rates because i dont know how this works
|
|
r1 = 1 - r240 - r80 - r10
|
|
return Rates.model_validate(
|
|
{
|
|
"rates": {
|
|
"r_1": r1,
|
|
"r_10": r10,
|
|
"r_80": r80,
|
|
"r_240": r240,
|
|
}
|
|
}
|
|
)
|
|
|
|
@node(name="reset rating")
|
|
@staticmethod
|
|
def r0(state: ContextInput) -> Rates:
|
|
return Rates.model_validate({"rates": state.context["initial_rates"].copy()})
|
|
|
|
|
|
class CounterUp:
|
|
@node(name="counter 6* reset")
|
|
@staticmethod
|
|
def c80(_: Nothing) -> Counters:
|
|
return Counters.model_validate(
|
|
{
|
|
"counter": {
|
|
"c_80": 0,
|
|
"c_10": 0,
|
|
},
|
|
"simple_counter": 0,
|
|
# this is influenced by the add reducer.
|
|
# its top level. it doesnt reset. its a miracle. i hate this.
|
|
}
|
|
)
|
|
|
|
@node(name="counter 5* reset")
|
|
@staticmethod
|
|
def c10(c: Counters) -> Counters:
|
|
c80 = c.counter["c_80"]
|
|
return Counters.model_validate(
|
|
{
|
|
"counter": {"c_10": 0, "c_80": c80}, # merge with or_!
|
|
"simple_counter": 0,
|
|
}
|
|
)
|
|
|
|
@node(name="counting up")
|
|
@staticmethod
|
|
def c1(state: Counters) -> Counters:
|
|
c = state.counter
|
|
c10, c80 = c["c_10"], c["c_80"]
|
|
return Counters(
|
|
simple_counter=1,
|
|
counter={"c_10": (c10 + 1) % 10, "c_80": (c80 + 1) % 80},
|
|
)
|
|
|
|
|
|
class RatesContextInput(Rates, ContextInput): ...
|
|
|
|
|
|
@node(name="prepare pool")
|
|
def prep(state: RatesContextInput) -> CurrentPools:
|
|
r = state.rates
|
|
p = state.context["pool"]
|
|
t: Final[tuple[tuple[str, str, str], ...]] = ( # greatest hack
|
|
("1", "r_1", "n_1"),
|
|
("10", "r_10", "n_10"),
|
|
("80", "r_80", "n_80"),
|
|
("240", "r_240", "n_240"),
|
|
)
|
|
pbc = [PoolByCategory(pool=p[pc], category=ty, rates=r[pr]) for ty, pr, pc in t]
|
|
# print(pbc)
|
|
return CurrentPools.model_validate({"current_pools": pbc})
|
|
|
|
|
|
class ThisStorage(Storage, CurrentRoll): ...
|
|
|
|
|
|
@node
|
|
def roll(state: CurrentPools) -> ThisStorage:
|
|
r = state.current_pools
|
|
(t,) = random.choices(r, weights=[*map(lambda p: p["rates"], r)])
|
|
this = Entity(category=t["category"], name=random.choice(t["pool"]))
|
|
return ThisStorage.model_validate(
|
|
{
|
|
"this": this,
|
|
"storage": [this], # I NEED MERGE
|
|
}
|
|
)
|
|
|
|
|
|
@node(outcomes=("240", "80", "10", "1")) # missed this! good job.
|
|
def post_roll_router(
|
|
state: CurrentRoll,
|
|
) -> NoOutput: # Literal["240", "80", "10", "1"] maybe you need to encode this in node returns. Literal of strings.
|
|
return outcome(state.this["category"])
|
|
|
|
|
|
@node(name="main")
|
|
def tick(state: Countdown) -> Countdown:
|
|
"""Emit a countdown delta because State.countdown uses the add reducer."""
|
|
return Countdown(countdown=-1)
|
|
|
|
|
|
@node(outcomes=("tick", END))
|
|
def keep_rolling(state: Countdown) -> NodeReturn[Nothing]:
|
|
return outcome("tick") if (state.countdown or 0) > 0 else outcome(END)
|
|
|
|
|
|
# there is like no general uses for the nodes; idk tho
|