2424 import pathlib
2525
2626
27+ _RANDOM_OPTS = [
28+ "random" , "random-low" , "random-middle" , "random-high" ,
29+ "random-range-low-<min>-<max>" , "random-range-middle-<min>-<max>" ,
30+ "random-range-high-<min>-<max>" , "random-range-<min>-<max>" ,
31+ ]
32+
33+
34+ def triangular (lower : int , end : int , tri : float = 0.5 ) -> int :
35+ """
36+ Integer triangular distribution for `lower` inclusive to `end` inclusive.
37+
38+ Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
39+ """
40+ # Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
41+ # random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
42+ # when a != b, so ensure the result is never more than `end`.
43+ return min (end , math .floor (random .triangular (0.0 , 1.0 , tri ) * (end - lower + 1 ) + lower ))
44+
45+
46+ def random_weighted_range (text : str , range_start : int , range_end : int ):
47+ if text == "random-low" :
48+ return triangular (range_start , range_end , 0.0 )
49+ elif text == "random-high" :
50+ return triangular (range_start , range_end , 1.0 )
51+ elif text == "random-middle" :
52+ return triangular (range_start , range_end )
53+ elif text == "random" :
54+ return random .randint (range_start , range_end )
55+ else :
56+ raise Exception (f"random text \" { text } \" did not resolve to a recognized pattern. "
57+ f"Acceptable values are: { ', ' .join (_RANDOM_OPTS )} ." )
58+
59+
2760def roll_percentage (percentage : int | float ) -> bool :
2861 """Roll a percentage chance.
2962 percentage is expected to be in range [0, 100]"""
@@ -690,12 +723,6 @@ class Range(NumericOption):
690723 range_start = 0
691724 range_end = 1
692725
693- _RANDOM_OPTS = [
694- "random" , "random-low" , "random-middle" , "random-high" ,
695- "random-range-low-<min>-<max>" , "random-range-middle-<min>-<max>" ,
696- "random-range-high-<min>-<max>" , "random-range-<min>-<max>" ,
697- ]
698-
699726 def __init__ (self , value : int ):
700727 if value < self .range_start :
701728 raise Exception (f"{ value } is lower than minimum { self .range_start } for option { self .__class__ .__name__ } " )
@@ -744,40 +771,26 @@ def from_text(cls, text: str) -> Range:
744771
745772 @classmethod
746773 def weighted_range (cls , text ) -> Range :
747- if text == "random-low" :
748- return cls (cls .triangular (cls .range_start , cls .range_end , 0.0 ))
749- elif text == "random-high" :
750- return cls (cls .triangular (cls .range_start , cls .range_end , 1.0 ))
751- elif text == "random-middle" :
752- return cls (cls .triangular (cls .range_start , cls .range_end ))
753- elif text .startswith ("random-range-" ):
774+ if text .startswith ("random-range-" ):
754775 return cls .custom_range (text )
755- elif text == "random" :
756- return cls (random .randint (cls .range_start , cls .range_end ))
757776 else :
758- raise Exception (f"random text \" { text } \" did not resolve to a recognized pattern. "
759- f"Acceptable values are: { ', ' .join (cls ._RANDOM_OPTS )} ." )
777+ return cls (random_weighted_range (text , cls .range_start , cls .range_end ))
760778
761779 @classmethod
762780 def custom_range (cls , text ) -> Range :
763781 textsplit = text .split ("-" )
764782 try :
765- random_range = [int (textsplit [len ( textsplit ) - 2 ]), int (textsplit [len ( textsplit ) - 1 ])]
783+ random_range = [int (textsplit [- 2 ]), int (textsplit [- 1 ])]
766784 except ValueError :
767785 raise ValueError (f"Invalid random range { text } for option { cls .__name__ } " )
768786 random_range .sort ()
769787 if random_range [0 ] < cls .range_start or random_range [1 ] > cls .range_end :
770788 raise Exception (
771789 f"{ random_range [0 ]} -{ random_range [1 ]} is outside allowed range "
772790 f"{ cls .range_start } -{ cls .range_end } for option { cls .__name__ } " )
773- if text .startswith ("random-range-low" ):
774- return cls (cls .triangular (random_range [0 ], random_range [1 ], 0.0 ))
775- elif text .startswith ("random-range-middle" ):
776- return cls (cls .triangular (random_range [0 ], random_range [1 ]))
777- elif text .startswith ("random-range-high" ):
778- return cls (cls .triangular (random_range [0 ], random_range [1 ], 1.0 ))
779- else :
780- return cls (random .randint (random_range [0 ], random_range [1 ]))
791+ if textsplit [2 ] in ("low" , "middle" , "high" ):
792+ return cls (random_weighted_range (f"{ textsplit [0 ]} -{ textsplit [2 ]} " , * random_range ))
793+ return cls (random_weighted_range ("random" , * random_range ))
781794
782795 @classmethod
783796 def from_any (cls , data : typing .Any ) -> Range :
@@ -792,18 +805,6 @@ def get_option_name(cls, value: int) -> str:
792805 def __str__ (self ) -> str :
793806 return str (self .value )
794807
795- @staticmethod
796- def triangular (lower : int , end : int , tri : float = 0.5 ) -> int :
797- """
798- Integer triangular distribution for `lower` inclusive to `end` inclusive.
799-
800- Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
801- """
802- # Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
803- # random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
804- # when a != b, so ensure the result is never more than `end`.
805- return min (end , math .floor (random .triangular (0.0 , 1.0 , tri ) * (end - lower + 1 ) + lower ))
806-
807808
808809class NamedRange (Range ):
809810 special_range_names : typing .Dict [str , int ] = {}
@@ -1000,13 +1001,19 @@ def __contains__(self, item):
10001001class OptionSet (Option [typing .Set [str ]], VerifyKeys ):
10011002 default = frozenset ()
10021003 supports_weighting = False
1004+ random_str : str | None
10031005
1004- def __init__ (self , value : typing .Iterable [str ]):
1006+ def __init__ (self , value : typing .Iterable [str ], random_str : str | None = None ):
10051007 self .value = set (deepcopy (value ))
1008+ self .random_str = random_str
10061009 super (OptionSet , self ).__init__ ()
10071010
10081011 @classmethod
10091012 def from_text (cls , text : str ):
1013+ check_text = text .lower ().split ("," )
1014+ if ((cls .valid_keys or cls .verify_item_name or cls .verify_location_name )
1015+ and len (check_text ) == 1 and check_text [0 ].startswith ("random" )):
1016+ return cls ((), check_text [0 ])
10101017 return cls ([option .strip () for option in text .split ("," )])
10111018
10121019 @classmethod
@@ -1015,6 +1022,35 @@ def from_any(cls, data: typing.Any):
10151022 return cls (data )
10161023 return cls .from_text (str (data ))
10171024
1025+ def verify (self , world : typing .Type [World ], player_name : str , plando_options : PlandoOptions ) -> None :
1026+ if self .random_str and not self .value :
1027+ choice_list = sorted (self .valid_keys )
1028+ if self .verify_item_name :
1029+ choice_list .extend (sorted (world .item_names ))
1030+ if self .verify_location_name :
1031+ choice_list .extend (sorted (world .location_names ))
1032+ if self .random_str .startswith ("random-range-" ):
1033+ textsplit = self .random_str .split ("-" )
1034+ try :
1035+ random_range = [int (textsplit [- 2 ]), int (textsplit [- 1 ])]
1036+ except ValueError :
1037+ raise ValueError (f"Invalid random range { self .random_str } for option { self .__class__ .__name__ } "
1038+ f"for player { player_name } " )
1039+ random_range .sort ()
1040+ if random_range [0 ] < 0 or random_range [1 ] > len (choice_list ):
1041+ raise Exception (
1042+ f"{ random_range [0 ]} -{ random_range [1 ]} is outside allowed range "
1043+ f"0-{ len (choice_list )} for option { self .__class__ .__name__ } for player { player_name } " )
1044+ if textsplit [2 ] in ("low" , "middle" , "high" ):
1045+ choice_count = random_weighted_range (f"{ textsplit [0 ]} -{ textsplit [2 ]} " ,
1046+ random_range [0 ], random_range [1 ])
1047+ else :
1048+ choice_count = random_weighted_range ("random" , random_range [0 ], random_range [1 ])
1049+ else :
1050+ choice_count = random_weighted_range (self .random_str , 0 , len (choice_list ))
1051+ self .value = set (random .sample (choice_list , k = choice_count ))
1052+ super (Option , self ).verify (world , player_name , plando_options )
1053+
10181054 @classmethod
10191055 def get_option_name (cls , value ):
10201056 return ", " .join (sorted (value ))
0 commit comments