└── addons └── dice_syntax ├── array_logic.gd ├── dice_helpers.gd ├── dice_syntax.gd ├── plugin.cfg ├── plugin.gd ├── single_dice_funs.gd └── string_manip.gd /addons/dice_syntax/array_logic.gd: -------------------------------------------------------------------------------- 1 | extends GDScript 2 | # functions meant to deal with numeric arrays 3 | 4 | 5 | # check if all is true in an array 6 | static func all(array:Array)->bool: 7 | var out = true 8 | for x in array: 9 | if !x: 10 | out = false 11 | return out 12 | 13 | # true if any is true in an array 14 | static func any(array:Array)->bool: 15 | var out = false 16 | for x in array: 17 | if x: 18 | out = true 19 | return out 20 | 21 | # sum of trues in an array 22 | static func sum_bool(array:Array)->int: 23 | var out:int = 0 24 | for x in array: 25 | if x: 26 | out +=1 27 | return out 28 | 29 | # sum of numbers in the array 30 | static func sum(array:Array)->float: 31 | var out:float = 0 32 | for x in array: 33 | out += x 34 | return(out) 35 | 36 | # get mean of array 37 | static func mean(array:Array)->float: 38 | var out:float = 0 39 | return float(sum(array))/array.size() 40 | 41 | # multiply array elements by factor 42 | static func multiply_array(array:Array,mult:float)->Array: 43 | var out:Array 44 | for x in array: 45 | out.append(x*mult) 46 | return out 47 | 48 | # add a number to array elements 49 | static func add_to_array(array:Array,add:float)->Array: 50 | var out:Array 51 | for x in array: 52 | out.append(x+add) 53 | return out 54 | 55 | # check if elements of an array are in another array 56 | static func array_in_array(array:Array,target:Array)->Array: 57 | var out:Array 58 | for i in range(array.size()): 59 | out.append(target.find(array[i]) != -1) 60 | return out 61 | 62 | # return which indexes contain the elemnts of an array 63 | static func which_in_array(array:Array,target:Array)->Array: 64 | var out:Array 65 | for i in range(array.size()): 66 | if target.find(array[i]) != -1: 67 | out.append(i) 68 | return out 69 | 70 | # return true indexes in an array 71 | static func which(array:Array)->Array: 72 | var out:Array 73 | for i in range(array.size()): 74 | if array[i]: 75 | out.append(i) 76 | return out 77 | 78 | static func order(array:Array)->Array: 79 | var out:Array 80 | var sorted = array.duplicate() 81 | sorted.sort() 82 | for x in array: 83 | out.append(sorted.find(x)) 84 | return out 85 | 86 | static func array_compare(array:Array,reference:float,equal:bool = false,less:bool = false, greater:bool = false)->Array: 87 | var out:Array 88 | 89 | for x in array: 90 | var result = false 91 | if equal: 92 | if x == reference: 93 | result = true 94 | if less: 95 | if x < reference: 96 | result = true 97 | if greater: 98 | if x > reference: 99 | result = true 100 | out.append(result) 101 | 102 | 103 | return out 104 | 105 | static func array_not(array:Array)->Array: 106 | var out:Array 107 | for x in array: 108 | out.append(not x) 109 | return out 110 | 111 | static func array_subset(array:Array, indices:Array)->Array: 112 | var out:Array 113 | for i in indices: 114 | out.append(array[i]) 115 | return out 116 | 117 | static func sample(array:Array,n:int,rng:RandomNumberGenerator, replace:bool = true)->Array: 118 | var out:Array = [] 119 | if not replace and n>array.size(): 120 | push_error('Cannot take a sample larger than the population when replace = false') 121 | return out 122 | for i in range(n): 123 | var samp = rng.randi_range(0,array.size()-1) 124 | out.append(array[samp]) 125 | if not replace: 126 | array.remove_at(samp) 127 | 128 | 129 | 130 | return out 131 | 132 | # append single elements/arrays with each other 133 | static func append(e1,e2)->Array: 134 | var out = [] 135 | if typeof(e1)==TYPE_ARRAY: 136 | out.append_array(e1) 137 | else: 138 | out.append(e1) 139 | if typeof(e2)==TYPE_ARRAY: 140 | out.append_array(e2) 141 | else: 142 | out.append(e2) 143 | return out 144 | 145 | # sample from an array using given weights 146 | static func sample_weights(array:Array, weights: Array, n:int, rng:RandomNumberGenerator, replace:bool = true)->Array: 147 | var out:Array = [] 148 | var sm = sum(weights) 149 | weights[0] = float(weights[0])/float(sm) 150 | for i in range(1,weights.size()): 151 | weights[i] = float(weights[i])/float(sm) + weights[i-1] 152 | 153 | 154 | for i in range(n): 155 | var rand = rng.randf() 156 | var curr_index = 0 157 | while rand > weights[curr_index]: 158 | curr_index += 1 159 | out.append(array[curr_index]) 160 | 161 | return out 162 | 163 | static func table(array:Array) -> Dictionary: 164 | var out:Dictionary 165 | 166 | for x in array: 167 | if x in out.keys(): 168 | out[x] = out[x]+1 169 | else: 170 | out[x] = 1 171 | 172 | return out 173 | 174 | static func tests(): 175 | print('testing logic functions') 176 | var true_true = [true,true] 177 | var true_false = [true,false] 178 | var false_false = [false,false] 179 | 180 | assert(sum_bool(true_true) == 2,"bad sum") 181 | assert(sum_bool(true_false) == 1,"bad sum") 182 | assert(sum_bool(false_false) == 0, 'bad sum') 183 | assert(any(true_false),'bad any') 184 | assert(all(true_true),'bad all') 185 | assert(!all(true_false),'bad all') 186 | assert(any(array_in_array(true_false,true_true)),'bad array_in_array') 187 | assert(!all(array_in_array(true_false,true_true)),'bad array_in_array') 188 | -------------------------------------------------------------------------------- /addons/dice_syntax/dice_helpers.gd: -------------------------------------------------------------------------------- 1 | extends GDScript 2 | 3 | # convert integer to a different base, return the result as an array of digits 4 | static func base_convert(number:int,base:int)->Array: 5 | var current_num = number 6 | var remainder:int 7 | var result:int 8 | var digits = [] 9 | while true: 10 | result = current_num/base 11 | remainder = current_num%base 12 | digits.append(remainder) 13 | if result > remainder: 14 | current_num = result 15 | else: 16 | if result != 0: 17 | digits.append(result) 18 | break 19 | digits.reverse() 20 | return digits 21 | 22 | # convert integer inter to base 26 and return as letters for use as names in expressions 23 | static func int_to_letter(number:int)->String: 24 | var letters = 'zabcdefghijklmnopqrstuvwxy' 25 | var num_array = base_convert(number,letters.length()) 26 | var out:String 27 | 28 | for x in num_array: 29 | out += letters[x] 30 | 31 | return out 32 | 33 | # keys are dice totals 34 | static func merge_probs(prob1:Dictionary, prob2:Dictionary)-> Dictionary: 35 | var out:Dictionary 36 | for i in prob1.keys(): 37 | for j in prob2.keys(): 38 | add_to_dict(out,i+j,prob1[i]*prob2[j]) 39 | return out 40 | 41 | # keys are individual dice 42 | static func merge_probs_keep_dice(prob1:Dictionary, prob2:Dictionary, sort:bool = true)->Dictionary: 43 | var al = preload('array_logic.gd') 44 | var out:Dictionary 45 | for i in prob1.keys(): 46 | for j in prob2.keys(): 47 | var new_key:Array = al.append(i,j) 48 | if sort: 49 | new_key.sort() 50 | add_to_dict(out,new_key,prob1[i]*prob2[j]) 51 | 52 | # out[new_key] = prob1[i]*prob2[j] 53 | return out 54 | 55 | # if element exists in dictionary, add to the existing value, if not create element 56 | static func add_to_dict(dict:Dictionary,key,value)->void: 57 | if dict.has(key): 58 | dict[key] += value 59 | else: 60 | dict[key] = value 61 | 62 | # calculations for exploding and compounding dice 63 | static func blow_up(probs:Dictionary,blow_dice:Array, depth = 3)-> Dictionary: 64 | var base_probs = probs.duplicate() 65 | for d in range(depth-1): 66 | var blown_up:Dictionary 67 | for k in probs.keys(): 68 | if k.back() in blow_dice: 69 | for i in base_probs.keys(): 70 | var new_key = k.duplicate() 71 | new_key.append_array(i) 72 | var new_value = base_probs[i]*probs[k] 73 | blown_up[new_key] = new_value 74 | else: 75 | blown_up[k] = probs[k] 76 | probs = blown_up 77 | 78 | return probs 79 | 80 | static func collapse_probs(probs:Dictionary, array_keys:bool = true)-> Dictionary: 81 | var al = preload('array_logic.gd') 82 | var out: Dictionary 83 | var temp: Dictionary 84 | for k in probs.keys(): 85 | var new_key = al.sum(k) 86 | add_to_dict(temp,new_key,probs[k]) 87 | 88 | # if returned dictionary should have arrays as keys, re-transform 89 | if array_keys: 90 | for k in temp.keys(): 91 | out[[k]] = temp[k] 92 | else: 93 | out = temp 94 | 95 | return out 96 | 97 | # add error information to the output if something goes wrong. 98 | # dictionaries are passed by reference 99 | static func dice_error(condition:bool,message:String,rolling_rules:Dictionary)->void: 100 | if(!condition): 101 | push_error(message) 102 | rolling_rules['error'] = true 103 | rolling_rules['msg'].append(message) 104 | 105 | 106 | static func range_determine(token:String,dice_side:int,regex:RegEx = RegEx.new(),rolling_rules:Dictionary={} ,default:int = 1)-> PackedInt64Array: 107 | var sm = preload('string_manip.gd') 108 | var out:PackedInt64Array = [] 109 | var number = sm.str_extract(token,'[0-9]*$', regex) 110 | dice_error(!(sm.str_detect(token,'<',regex) and sm.str_detect(token,">",regex)),'Invalid dice: A range clause can only have one of "<" or ">"',rolling_rules) 111 | dice_error(!(sm.str_detect(token,'<|>|=',regex) and number ==''),'Invalid dice: Using "<", ">" or "=" operators requires an integer',rolling_rules) 112 | if !sm.str_detect(token,"<|>",regex) and number == '': 113 | out.append(default) 114 | elif number != '' and !sm.str_detect_rg(token, regex): 115 | out.append(number.to_int()) 116 | elif sm.str_detect(token,"<" ,regex) and number != '': 117 | out.append_array(range(1,number.to_int()+1)) 118 | elif sm.str_detect(token,">", regex) and number != '': 119 | out.append_array(range(number.to_int(),dice_side+1)) 120 | 121 | return out 122 | -------------------------------------------------------------------------------- /addons/dice_syntax/dice_syntax.gd: -------------------------------------------------------------------------------- 1 | extends GDScript 2 | class_name dice_syntax 3 | 4 | 5 | 6 | 7 | 8 | static func dice_parser(dice:String,regex:RegEx = RegEx.new(), base_instance = null)->Dictionary: 9 | var sm = preload('string_manip.gd') 10 | var sdf = preload('single_dice_funs.gd') 11 | var dh = preload('dice_helpers.gd') 12 | var error = false 13 | var msg = [] 14 | var dice_regex = '[0-9]*d[0-9]+[dksfro/!<=>0-9lh]*' 15 | 16 | 17 | regex.compile(dice_regex) 18 | var dice_components = sm.str_extract_all_rg(dice,regex) 19 | var dice_expression_compoments = sm.str_split_rg(dice,regex) 20 | var dice_expression = '' 21 | var dice_letters = [] 22 | for i in range(dice_expression_compoments.size()): 23 | dice_expression += dice_expression_compoments[i] 24 | if i < dice_components.size(): 25 | dice_letters.append(dh.int_to_letter(i)) 26 | dice_expression += dice_letters[i] 27 | var rules_array = [] 28 | for x in dice_components: 29 | var rr = sdf.base_dice_parser(x,regex) 30 | if rr.error: 31 | error = true 32 | msg.append_array(rr.msg) 33 | 34 | rules_array.append(rr) 35 | 36 | var expression = Expression.new() 37 | expression.parse(dice_expression,dice_letters) 38 | 39 | # test execution to see if it's valid 40 | var test_out = [] 41 | for i in range(dice_letters.size()): 42 | test_out.append(1.0) 43 | 44 | expression.execute(test_out, base_instance) 45 | if expression.has_execute_failed() or expression.get_error_text()!='': 46 | error = true 47 | msg.append('Expression fails to execute') 48 | 49 | 50 | var bad_regex = "(^|[+\\-*/% ])[0-9]+[a-zA-Z]" 51 | regex.compile(bad_regex) 52 | 53 | if sm.str_detect_rg(dice_expression,regex): 54 | error = true 55 | msg.append('Invalid numeric notation') 56 | 57 | 58 | return { 59 | 'rules_array':rules_array, 60 | 'dice_expression':expression, 61 | 'expression_string':dice_expression, 62 | 'error':error, 63 | 'msg':msg} 64 | 65 | static func roll_parsed(rules:Dictionary, rng:RandomNumberGenerator = RandomNumberGenerator.new(), base_instance = null)->Dictionary: 66 | var sdf = preload('single_dice_funs.gd') 67 | var results:Array 68 | var roll_sums:Array 69 | var error = rules.error 70 | var msg = rules.msg 71 | 72 | for i in range(rules.rules_array.size()): 73 | var result = sdf.base_rule_roller(rules.rules_array[i],rng) 74 | results.append(result) 75 | roll_sums.append(result.result) 76 | 77 | var sum = rules.dice_expression.execute(roll_sums, base_instance) 78 | 79 | if error: 80 | sum = 0 81 | 82 | return {'result':sum, 'rolls':results,'error': error, 'msg': msg} 83 | 84 | static func roll(dice:String, rng:RandomNumberGenerator = RandomNumberGenerator.new(),regex = RegEx.new() ,base_instance = null)->Dictionary: 85 | var rules = dice_parser(dice,regex,base_instance) 86 | return roll_parsed(rules,rng,base_instance) 87 | 88 | static func parsed_dice_probs(rules, explode_depth:int=1, base_instance = null)->Dictionary: 89 | var dh = preload('dice_helpers.gd') 90 | var al = preload('array_logic.gd') 91 | var sdf = preload('single_dice_funs.gd') 92 | var final_result = {} 93 | var error = rules.error 94 | for i in range(rules.rules_array.size()): 95 | var result = sdf.base_calc_rule_probs(rules.rules_array[i],explode_depth) 96 | if i == 0: # if it's the first iteration populate the dictionary 97 | for x in result.keys(): 98 | final_result[[x]] = result[x] 99 | else: 100 | final_result = dh.merge_probs_keep_dice(final_result,result,false) 101 | 102 | if error: 103 | return {0.0:1.0} 104 | 105 | var processed_results = {} 106 | for x in final_result.keys(): 107 | var new_key = rules.dice_expression.execute(x, base_instance) 108 | dh.add_to_dict(processed_results,new_key,final_result[x]) 109 | 110 | if final_result.size()==0: 111 | processed_results[float(rules.dice_expression.execute([],base_instance))] = 1 112 | 113 | return processed_results 114 | 115 | static func dice_probs(dice:String,explode_depth:int=1,regex = RegEx.new(),base_instance = null)->Dictionary: 116 | var rules = dice_parser(dice,regex,base_instance) 117 | return parsed_dice_probs(rules,explode_depth,base_instance) 118 | 119 | 120 | static func expected_value(probs:Dictionary)->float: 121 | var out = 0 122 | for k in probs.keys(): 123 | out += probs[k]*float(k) 124 | return(out) 125 | 126 | static func standard_deviation(probs:Dictionary)->float: 127 | var out = 0 128 | var mean = expected_value(probs) 129 | for k in probs.keys(): 130 | out += pow(float(k)-mean,2)*probs[k] 131 | return pow(out,0.5) 132 | 133 | static func roll_from_probs(probs:Dictionary,rng:RandomNumberGenerator = RandomNumberGenerator.new(),n=1)->Array: 134 | var al = preload('array_logic.gd') 135 | return al.sample_weights(probs.keys(),probs.values(),n,rng) 136 | 137 | -------------------------------------------------------------------------------- /addons/dice_syntax/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="dice_syntax" 4 | description="Dice rolling syntax" 5 | author="Ogan Mancarci" 6 | version="3.1.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/dice_syntax/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | func _enter_tree(): 6 | pass 7 | 8 | 9 | func _exit_tree(): 10 | pass 11 | -------------------------------------------------------------------------------- /addons/dice_syntax/single_dice_funs.gd: -------------------------------------------------------------------------------- 1 | extends GDScript 2 | # internal functions that parse single dice rolls 3 | 4 | 5 | # basic dice parser for single rolls 6 | 7 | static func base_dice_parser(dice_string:String,regex:RegEx = RegEx.new())->Dictionary: 8 | var sm = preload('string_manip.gd') 9 | var al = preload('array_logic.gd') 10 | var dh = preload('dice_helpers.gd') 11 | var rolling_rules: Dictionary = { 12 | 'error': false, 13 | 'msg': [], 14 | 'compound':[], 15 | 'explode':[], 16 | 'reroll_once': [], 17 | 'reroll': [], 18 | 'possible_dice': [], 19 | 'drop_dice':0, 20 | 'drop_lowest':true, 21 | 'drop_keep_specific': [], 22 | 'drop_specific': true, 23 | 'dice_side':0, 24 | 'dice_count':0, 25 | 'success': [], 26 | 'fail': [], 27 | 'explode_depth': 0, 28 | 'sort':false 29 | } 30 | var valid_tokens = '[dksfr/!<=>hl]' 31 | dice_string = dice_string.to_lower() 32 | var used_tokens:PackedInt64Array 33 | 34 | # get the dice count or default to 1 if we just start with d. 35 | var result = sm.str_extract(dice_string,'^[0-9]*?(?=d)',regex) 36 | dh.dice_error(result!=null,'Invalid dice: Cannot determine dice count',rolling_rules) 37 | if result == '': 38 | rolling_rules['dice_count'] = 1 39 | elif result == null: 40 | return rolling_rules 41 | elif result.is_valid_int(): 42 | rolling_rules['dice_count'] = result.to_int() 43 | 44 | # tokenize the rest of the rolling rules. a token character, followed by the 45 | # next valid token character or end of string 46 | var tokens:PackedStringArray = sm.str_extract_all(dice_string, 47 | valid_tokens + '.*?((?=' + valid_tokens + ')|$)',regex) 48 | var dice_side = sm.str_extract(tokens[0],'(?<=d)[0-9]+$',regex) 49 | dh.dice_error(dice_side != null, "Invalid dice: Unable to detect dice sides",rolling_rules) 50 | if dice_side!=null: 51 | rolling_rules['dice_side'] = dice_side.to_int() 52 | else: 53 | return rolling_rules 54 | # remove dice side token to make sure it's not confused with the drop rule 55 | tokens.remove_at(0) 56 | 57 | 58 | 59 | 60 | # check for drop modifiers 61 | var drop_modifiers:PackedInt64Array = sm.strs_detect(tokens,'^(h|l)[0-9]+$',regex) 62 | # check for range specifications 63 | var ranges:Array = sm.strs_detect(tokens,"^[<=>][0-9]+$",regex) 64 | 65 | # s can mean sort or success depending on context 66 | var sort_or_success = sm.strs_detect(tokens,'^s[0-9]*$',regex) 67 | var success:Array = [] 68 | for i in sort_or_success: 69 | used_tokens.append(i) 70 | var token:String = tokens[i] 71 | var is_sort:bool = true 72 | if i+1 in ranges and !sm.str_detect(token,"[0-9]",regex): 73 | used_tokens.append(i+1) 74 | token = token + tokens[i+1] 75 | is_sort = false 76 | elif sm.str_detect(token,"[0-9]",regex): 77 | is_sort = false 78 | 79 | if is_sort: 80 | dh.dice_error(!rolling_rules['sort'], "Invalid dice: Can't sort twice",rolling_rules) 81 | rolling_rules['sort'] = true 82 | else: 83 | success.append_array(dh.range_determine(token, rolling_rules['dice_side'],regex,rolling_rules)) 84 | rolling_rules['success'] = success 85 | 86 | # fail range 87 | var fail_tokens = sm.strs_detect(tokens,'^f[0-9]*$',regex) 88 | var fail:Array = [] 89 | for i in fail_tokens: 90 | used_tokens.append(i) 91 | var fail_token = tokens[i] 92 | dh.dice_error(i+1 in ranges or sm.str_detect(fail_token,"[0-9]",regex), "Invalid dice: Fails must include a range",rolling_rules) 93 | if i+1 in ranges and !sm.str_detect(fail_token,"[0-9]",regex): 94 | used_tokens.append(i+1) 95 | fail_token = fail_token + tokens[i+1] 96 | 97 | fail.append_array(dh.range_determine(fail_token, rolling_rules['dice_side'],regex,rolling_rules)) 98 | rolling_rules['fail'] = fail 99 | 100 | 101 | 102 | # drop rules 103 | var drop_rules:Array = sm.strs_detect(tokens,'^(d|k)[0-9]*$',regex) 104 | for i in drop_rules: 105 | used_tokens.append(i) 106 | # look for the drop count in the current token 107 | var drop_count = sm.str_extract(tokens[i],'[0-9]+$', regex) 108 | var drop_rule:String = tokens[i] 109 | # if drop count isn't found 110 | if drop_count == null and i+1 in drop_modifiers: 111 | # next token is a drop modifier, it must come with a drop count 112 | dh.dice_error(rolling_rules['drop_dice'] == 0, "Invalid dice: Can't include more than one drop count",rolling_rules) 113 | drop_count = sm.str_extract_rg(tokens[i+1],regex) 114 | drop_rule = tokens[i] + tokens[i+1] 115 | used_tokens.append(i+1) 116 | elif drop_count == null and i+1 in ranges: 117 | # next token is a range. drop specific results 118 | var drop_range = dh.range_determine(tokens[i+1],rolling_rules['dice_side'],regex,rolling_rules) 119 | match drop_rule.substr(0,1): 120 | "d": 121 | dh.dice_error(rolling_rules['drop_specific'],"Invalid dice: Can't specify both dropping and keeping specific dice",rolling_rules) 122 | rolling_rules['drop_specific'] = true 123 | pass 124 | 'k': 125 | dh.dice_error(!rolling_rules['drop_specific'] or rolling_rules['drop_keep_specific'].size()==0,"Invalid dice: Can't specify both dropping and keeping specific dice",rolling_rules) 126 | rolling_rules['drop_specific'] = false 127 | pass 128 | rolling_rules['drop_keep_specific'].append_array(drop_range) 129 | used_tokens.append(i+1) 130 | drop_count = "" 131 | 132 | dh.dice_error(drop_count!= null, 'Invalid: No drop count provided',rolling_rules) 133 | # dropping specific results will set drop count to "", if not, we still 134 | # need to process the drop count 135 | if drop_count!="" and drop_count != null: 136 | match drop_rule.substr(0,1): 137 | 'd': 138 | rolling_rules['drop_dice'] = drop_count.to_int() 139 | 'k': 140 | rolling_rules['drop_dice'] = rolling_rules['dice_count']-drop_count.to_int() 141 | 142 | regex.compile('dh') 143 | var dl1 = sm.str_detect_rg(drop_rule,regex) 144 | regex.compile('kl') 145 | var dl2 = sm.str_detect_rg(drop_rule,regex) 146 | rolling_rules['drop_lowest'] = !(dl1 or dl2) 147 | 148 | # reroll rules 149 | var reroll_rules = sm.strs_detect(tokens,'^r(?!o)[0-9]*$',regex) 150 | var reroll:Array = [] 151 | for i in reroll_rules: 152 | used_tokens.append(i) 153 | var reroll_token = tokens[i] 154 | if i+1 in ranges and !sm.str_detect(reroll_token,"[0-9]",regex): 155 | used_tokens.append(i+1) 156 | reroll_token = reroll_token+tokens[i+1] 157 | reroll.append_array(dh.range_determine(reroll_token, rolling_rules['dice_side'],regex,rolling_rules)) 158 | 159 | var dicePossibilities = range(1,rolling_rules['dice_side']+1) 160 | # dice_error(!al.all(al.array_in_array(dicePossibilities,reroll)),'Malformed dice string: rerolling all results',rolling_rules) 161 | rolling_rules['reroll'] = reroll 162 | # remove reroll rules 163 | 164 | 165 | # reroll once 166 | reroll_rules = sm.strs_detect(tokens,'^ro[0-9]*$',regex) 167 | var reroll_once:Array = [] 168 | for i in reroll_rules: 169 | used_tokens.append(i) 170 | var reroll_token = tokens[i] 171 | if i+1 in ranges and !sm.str_detect(reroll_token,"[0-9]",regex): 172 | used_tokens.append(i+1) 173 | reroll_token = reroll_token+tokens[i+1] 174 | 175 | reroll_once.append_array(dh.range_determine(reroll_token, rolling_rules['dice_side'],regex,rolling_rules)) 176 | 177 | dh.dice_error(al.table(reroll_once).values().filter(func(x):return x>1).size()==0, 178 | "Invalid dice: can't reroll the same number once more than once.", 179 | rolling_rules) 180 | rolling_rules['reroll_once'] = reroll_once 181 | 182 | 183 | # new explode rules 184 | regex.compile('^![0-9]*$') 185 | var explode_rules = sm.strs_detect_rg(tokens,regex) 186 | 187 | regex.compile("/") 188 | var end_rules = sm.strs_detect_rg(tokens,regex) 189 | 190 | var explode:Array = [] 191 | var compound: Array = [] 192 | var compound_flag:bool = false 193 | var depth_flag:bool = false 194 | var explode_depth:Array = [] 195 | 196 | 197 | 198 | for i in explode_rules: 199 | used_tokens.append(i) 200 | if tokens[i] == "!" and i + 1 in explode_rules and not compound_flag: 201 | compound_flag = true 202 | elif not compound_flag and i-1 not in end_rules: 203 | var explode_token = tokens[i] 204 | if i+1 in ranges and !sm.str_detect(explode_token,"[0-9]",regex): 205 | used_tokens.append(i+1) 206 | explode_token = explode_token + tokens[i+1] 207 | 208 | explode.append_array( 209 | dh.range_determine(explode_token, 210 | rolling_rules['dice_side'], 211 | regex,rolling_rules, 212 | rolling_rules['dice_side'])) 213 | elif not compound_flag and i-1 in end_rules: 214 | # /![number] 215 | used_tokens.append(i-1) 216 | var depth_token = tokens[i] 217 | if i+1 in ranges and !sm.str_detect(depth_token,"[0-9]",regex): 218 | used_tokens.append(i+1) 219 | depth_token = depth_token + tokens[i+1] 220 | 221 | var depths = dh.range_determine(depth_token, 222 | rolling_rules['dice_side'], 223 | regex,rolling_rules, 224 | rolling_rules['dice_side']) 225 | 226 | dh.dice_error(depths.size()==1,"Explode depth must be set to a single integer", rolling_rules) 227 | explode_depth.append_array(depths) 228 | 229 | elif compound_flag: 230 | compound_flag = false 231 | var compound_token = tokens[i] 232 | if i+1 in ranges and !sm.str_detect(compound_token,"[0-9]",regex): 233 | used_tokens.append(i+1) 234 | compound_token = compound_token + tokens[i+1] 235 | 236 | compound.append_array( 237 | dh.range_determine(compound_token, 238 | rolling_rules['dice_side'], 239 | regex,rolling_rules, 240 | rolling_rules['dice_side'])) 241 | 242 | if explode_depth.size()>0: 243 | rolling_rules['explode_depth'] = explode_depth.min() 244 | rolling_rules['explode'] = explode 245 | rolling_rules['compound'] = compound 246 | 247 | # all tokens are processed 248 | dh.dice_error(range(tokens.size()).all(func(x):return x in used_tokens), 249 | 'Invalid dice: Unprocessed tokens',rolling_rules) 250 | 251 | # every token is processed only once 252 | dh.dice_error(al.table(used_tokens).values().all(func(x):return x == 1), 253 | 'Invalid dice: Ambigious tokens', rolling_rules) 254 | 255 | 256 | var possible_dice = range(1,rolling_rules.dice_side+1) 257 | possible_dice = al.array_subset(possible_dice,al.which(al.array_not(al.array_in_array(possible_dice, rolling_rules.reroll)))) 258 | dh.dice_error(possible_dice.size()>0,"Invalid dice: No possible results",rolling_rules) 259 | dh.dice_error(!rolling_rules.success.any(func(x):return x in rolling_rules.fail), "Invalid dice: Cannot count same result as both success and failure",rolling_rules) 260 | 261 | dh.dice_error(possible_dice.size()==0 or not possible_dice.all(func(x):return x in rolling_rules.explode),"Invalid dice: can't explode every result",rolling_rules) 262 | dh.dice_error(possible_dice.size()==0 or not possible_dice.all(func(x):return x in rolling_rules.compound),"Invalid dice: can't compound every result",rolling_rules) 263 | dh.dice_error(al.which_in_array(rolling_rules.explode,rolling_rules.compound).size()==0,"Invalid dice: Can't explode what you compound.",rolling_rules) 264 | dh.dice_error(rolling_rules.drop_dice0 and rolling_rules.compound.size()>0), "Invalid dice: can't explode and compound with the same dice",rolling_rules) 268 | 269 | return rolling_rules 270 | 271 | # rolling a single roll from a parsed rules 272 | static func base_rule_roller(rolling_rules:Dictionary,rng:RandomNumberGenerator = RandomNumberGenerator.new())->Dictionary: 273 | var al = preload('array_logic.gd') 274 | var out:Dictionary = {'error': false, 275 | 'msg': [], 276 | 'dice': [], 277 | 'drop': [], 278 | 'result':0} 279 | 280 | if rolling_rules.error: 281 | out['error'] = true 282 | out['msg'] = rolling_rules.msg 283 | return out 284 | 285 | # setting the possible results with rerolls removed from possible results 286 | var possible_dice = rolling_rules.possible_dice 287 | 288 | # initial roll 289 | var dice = al.sample(possible_dice,rolling_rules.dice_count,rng) 290 | 291 | 292 | # reroll once 293 | var to_reroll = al.which_in_array(dice,rolling_rules.reroll_once) 294 | 295 | for i in to_reroll: 296 | dice[i] = al.sample(possible_dice,1,rng)[0] 297 | 298 | 299 | 300 | if rolling_rules.explode.size()>0: 301 | var exploded_dice = [] 302 | for d in dice: 303 | var x = d 304 | exploded_dice.append(d) 305 | var explode_count = 1 306 | while x in rolling_rules.explode and \ 307 | (rolling_rules.explode_depth == 0 or explode_count0: 317 | var compounded_dice = [] 318 | for d in dice: 319 | var com_result = d 320 | var x = d 321 | var explode_count = 1 322 | while x in rolling_rules.compound and \ 323 | (rolling_rules.explode_depth == 0 or explode_count0: 336 | var ordered_dice = dice.duplicate() 337 | ordered_dice.sort() 338 | var drop = [] 339 | if !rolling_rules.drop_lowest: 340 | ordered_dice.reverse() 341 | for i in range(0,rolling_rules.drop_dice): 342 | drop.append(ordered_dice[i]) 343 | var new_dice = [] 344 | var drop_copy = drop.duplicate() 345 | for x in dice: 346 | if not x in drop_copy: 347 | new_dice.append(x) 348 | else: 349 | drop_copy.remove_at(drop_copy.find(x)) 350 | dice = new_dice 351 | out['drop'] = drop 352 | 353 | if rolling_rules.drop_keep_specific.size()>0 and rolling_rules.drop_specific: 354 | out['drop'].append_array(dice.filter(func(x):return x in rolling_rules.drop_keep_specific)) 355 | dice = dice.filter(func(x):return not x in rolling_rules.drop_keep_specific) 356 | elif rolling_rules.drop_keep_specific.size()>0 and not rolling_rules.drop_specific: 357 | out['drop'].append_array(dice.filter(func(x):return not x in rolling_rules.drop_keep_specific)) 358 | dice = dice.filter(func(x):return x in rolling_rules.drop_keep_specific) 359 | 360 | out['dice'] = dice 361 | 362 | if rolling_rules.success.size()>0 or rolling_rules.fail.size()>0: 363 | var success = dice.filter(func(x):return x in rolling_rules.success) 364 | var fail = dice.filter(func(x):return x in rolling_rules.fail) 365 | out['result'] = float(success.size() - fail.size()) 366 | else: 367 | out['result'] = al.sum(dice) 368 | 369 | 370 | 371 | 372 | return out 373 | 374 | 375 | # calucate probabilities of a single roll 376 | static func base_calc_rule_probs(rules:Dictionary,explode_depth:int = 3)->Dictionary: 377 | var al = preload('array_logic.gd') 378 | var dh = preload('dice_helpers.gd') 379 | 380 | if rules.error: 381 | var probs = {0.0:1.0} 382 | return probs 383 | 384 | 385 | var base_prob = 1.0/rules.possible_dice.size() 386 | 387 | # base probabilities 388 | var probs:Dictionary 389 | for x in rules.possible_dice: 390 | probs[x] = base_prob 391 | 392 | 393 | # reroll once adjustment 394 | var reroll_prob = pow(base_prob, 2.0) 395 | var prob_to_add = 0 396 | for x in rules.reroll_once: 397 | probs[x] = 0 398 | prob_to_add += reroll_prob 399 | 400 | for x in probs.keys(): 401 | probs[x] += prob_to_add 402 | 403 | 404 | # transform keys into arrays for further processing 405 | var new_probs: Dictionary 406 | for x in probs.keys(): 407 | new_probs[[x]] = probs[x] 408 | probs = new_probs 409 | 410 | if rules.explode_depth != 0: 411 | print(explode_depth) 412 | print(rules.explode_depth) 413 | print(rules) 414 | explode_depth = min(explode_depth,rules.explode_depth) 415 | 416 | if rules.explode.size()>0: 417 | probs = dh.blow_up(probs,rules.explode, explode_depth) 418 | 419 | if rules.compound.size()>0: 420 | probs = dh.blow_up(probs,rules.compound, explode_depth) 421 | probs = dh.collapse_probs(probs,true) 422 | pass 423 | 424 | 425 | # rolling multiple dice 426 | var original_probs = probs.duplicate() 427 | for i in range(rules.dice_count-1): 428 | probs = dh.merge_probs_keep_dice(probs,original_probs) 429 | pass 430 | 431 | # drop dice 432 | if rules.drop_dice>0: 433 | var post_drop:Dictionary 434 | if rules.drop_lowest: 435 | for k in probs.keys(): 436 | var new_key = k.slice(rules.drop_dice,k.size()) 437 | dh.add_to_dict(post_drop,new_key,probs[k]) 438 | else: 439 | for k in probs.keys(): 440 | var new_key = k.slice(0,k.size()-rules.drop_dice) 441 | dh.add_to_dict(post_drop,new_key,probs[k]) 442 | probs = post_drop 443 | 444 | if rules.drop_keep_specific.size()>0 and rules.drop_specific: 445 | var post_drop_specific:Dictionary 446 | for k in probs.keys(): 447 | var new_key = k.filter(func(x):return not x in rules.drop_keep_specific) 448 | dh.add_to_dict(post_drop_specific,new_key,probs[k]) 449 | probs = post_drop_specific 450 | elif rules.drop_keep_specific.size()>0 and not rules.drop_specific: 451 | var post_drop_specific:Dictionary 452 | for k in probs.keys(): 453 | var new_key = k.filter(func(x):return x in rules.drop_keep_specific) 454 | dh.add_to_dict(post_drop_specific,new_key,probs[k]) 455 | probs = post_drop_specific 456 | 457 | if rules.success.size()>0 or rules.fail.size()>0: 458 | var post_success:Dictionary 459 | for k in probs.keys(): 460 | var success = k.filter(func(x):return x in rules.success) 461 | var fail = k.filter(func(x):return x in rules.fail) 462 | var new_key:Array 463 | new_key.append(success.size() - fail.size()) 464 | dh.add_to_dict(post_success,new_key,probs[k]) 465 | probs = post_success 466 | 467 | 468 | # collapse results into single sums 469 | probs = dh.collapse_probs(probs, false) 470 | 471 | return probs 472 | -------------------------------------------------------------------------------- /addons/dice_syntax/string_manip.gd: -------------------------------------------------------------------------------- 1 | extends GDScript 2 | 3 | 4 | static func str_extract(string:String,pattern:String,regex:RegEx = RegEx.new()): 5 | regex.compile(pattern) 6 | return str_extract_rg(string,regex) 7 | 8 | 9 | static func str_extract_rg(string:String,regex:RegEx): 10 | var result:RegExMatch = regex.search(string) 11 | if result == null: 12 | return null 13 | else: 14 | return result.get_string() 15 | 16 | static func str_extract_all(string:String,pattern:String,regex:RegEx = RegEx.new())-> PackedStringArray: 17 | regex.compile(pattern) 18 | return str_extract_all_rg(string,regex) 19 | 20 | 21 | static func str_extract_all_rg(string:String,regex:RegEx) -> PackedStringArray: 22 | var out:PackedStringArray 23 | for x in regex.search_all(string): 24 | out.append(x.get_string()) 25 | return(out) 26 | 27 | 28 | static func str_detect(string:String, pattern:String,regex:RegEx = RegEx.new()) -> bool: 29 | regex.compile(pattern) 30 | return str_detect_rg(string,regex) 31 | 32 | 33 | static func str_detect_rg(string:String, regex:RegEx)-> bool: 34 | var out:bool 35 | var result:RegExMatch = regex.search(string) 36 | return result != null 37 | 38 | 39 | static func str_split(string:String, pattern:String,regex:RegEx = RegEx.new())-> PackedStringArray: 40 | regex.compile(pattern) # Negated whitespace character class. 41 | return str_split_rg(string,regex) 42 | 43 | static func str_split_rg(string:String, regex:RegEx) -> PackedStringArray: 44 | var out:PackedStringArray = [] 45 | var start = 0 46 | var end = 0 47 | var next = 0 48 | for result in regex.search_all(string): 49 | end = result.get_start() 50 | next = result.get_end() 51 | out.append(string.substr(start,end-start)) 52 | start = next 53 | out.append(string.substr(start,-1)) 54 | 55 | return out 56 | 57 | 58 | # vectorized over an array of strings to return indexes of matching 59 | static func strs_detect(strings:Array,pattern:String,regex:RegEx = RegEx.new()) -> PackedInt64Array: 60 | regex.compile(pattern) 61 | return strs_detect_rg(strings,regex) 62 | 63 | static func strs_detect_rg(strings:Array,regex:RegEx) -> PackedInt64Array: 64 | var out:PackedInt64Array 65 | for i in range(strings.size()): 66 | if str_detect_rg(strings[i],regex): 67 | out.append(i) 68 | 69 | return out 70 | --------------------------------------------------------------------------------