extends RefCounted const E = preload("res://world/owniverse/math/exact.gd") ## Quantity arithmetic without scene tree or global settings. static var _weight_rules: Dictionary = {} static var _output_rules: Dictionary = {} ## Parsed rules are shared read-only; the string itself is the cache key. static func rule_weights(text: String) -> Dictionary: if _weight_rules.has(text): return _weight_rules[text] var result: Dictionary = {} for term in text.split("^", false): var pair: PackedStringArray = term.split(":") result[pair[0]] = E.of(pair[1]) if _weight_rules.size() >= 128: _weight_rules.clear() result.make_read_only() _weight_rules[text] = result return result static func output_terms(text: String) -> Array: if _output_rules.has(text): return _output_rules[text] var result: Array = [] for term in text.split("&"): var pair: PackedStringArray = term.strip_edges().split(":") if pair[0].strip_edges().is_empty(): continue result.append({"id": pair[0].strip_edges(), "amount": E.of(pair[1]) if E.eq(pair.size(), 2) else 1}) if _output_rules.size() >= 128: _output_rules.clear() result.make_read_only() _output_rules[text] = result return result static func accumulate_whole(target: Dictionary, field: String, key: String, value: Variant) -> Variant: if (not target.has(field)): target[field] = {} var expected: Variant = E.add(E.maximum(0, value), target[field].get(key, 0)) var whole: Variant = E.floor_value(expected) target[field][key] = E.maximum(0, E.subtract(expected, whole)) return whole static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary: var result: Dictionary = {} var total: Variant = 0 for id in requests: result[id] = E.floor_value(E.maximum(0, requests[id])) total = E.add(total, result[id]) return _allocate_whole_with_total(requests, limit, result, total) ## Whole stocks apportioned using float shares. Both the total and every stock ## ceiling are enforced exactly after quota rounding; no quantity becomes float. static func allocate_profiled(stocks: Dictionary, limit: Variant) -> Dictionary: var total: Variant = sum(stocks) var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total)) if E.eq(available, total): return stocks.duplicate() var ids: Array = stocks.keys() var shares: Array = [] var result: Dictionary = {} var only: Variant = null var positives: int = 0 for id in ids: result[id] = 0 if E.gt(stocks[id], 0): only = id positives += 1 if E.le(available, 0): return result if positives == 1: result[only] = available return result for id in ids: shares.append(E.ratio_approx(stocks[id], total)) result = split_profile({}, "stock", available, ids, shares) var assigned: Variant = 0 for id in ids: result[id] = E.minimum(stocks[id], result[id]) assigned = E.add(assigned, result[id]) var missing: Variant = E.subtract(available, assigned) for id in ids: if E.le(missing, 0): break var extra: Variant = E.minimum(missing, E.subtract(stocks[id], result[id])) result[id] = E.add(result[id], extra) missing = E.subtract(missing, extra) assert(E.eq(missing, 0), "Whole stock allocation must exhaust the available quota") return result ## Variant for callers that already maintain the sum of floored request values. ## The supplied total must equal sum(floor(max(0, requests[id]))). static func allocate_whole_with_total(requests: Dictionary, limit: Variant, request_total: Variant) -> Dictionary: var result: Dictionary = {} for id in requests: result[id] = E.floor_value(E.maximum(0, requests[id])) return _allocate_whole_with_total(requests, limit, result, request_total) static func _allocate_whole_with_total(requests: Dictionary, limit: Variant, result: Dictionary, total: Variant) -> Dictionary: var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total)) # No apportionment or rounding is necessary when every request fits. if E.eq(available, total): return result if E.le(available, 0): for id in result: result[id] = 0 return result var only: Variant = null var positive_count: int = 0 for id in result: if E.gt(result[id], 0): only = id positive_count += 1 if positive_count == 1: result[only] = available return result var ids: Array = requests.keys() var rests: Array = [] var order: Array = [] var assigned: Variant = 0 for i in ids.size(): var expected: Variant = E.mul_div(result[ids[i]], available, total) result[ids[i]] = E.floor_value(expected) assigned = E.add(assigned, result[ids[i]]) rests.append(E.subtract(expected, result[ids[i]])) order.append(i) order.sort_custom(func(a: int, b: int) -> bool: return E.gt(rests[a], rests[b]) if E.ne(rests[a], rests[b]) else str(ids[a]) < str(ids[b])) for i in order: if E.ge(assigned, available): break if E.lt(result[ids[i]], E.floor_value(requests[ids[i]])): result[ids[i]] = E.add(result[ids[i]], 1) assigned = E.add(assigned, 1) return result static func production_step(costs: Array): var step = 1 for cost in costs: if E.le(cost, 0): continue var terms: Array = E.parts(cost) var denominator = E.make(E.Big.divmod(terms[1], E.Big.gcd(terms[0], terms[1]))[0]) step = E.multiply(E.divide(step, whole_gcd(step, denominator)), denominator) return step static func whole_gcd(a: Variant, b: Variant) -> Variant: while E.gt(b, 0): var remainder: Variant = E.remainder(a, b) a = b b = remainder return a static func normalize_distribution(values: Dictionary) -> Dictionary: var normalized_distribution: Dictionary = {} var distribution_total: Variant = 0 for id in values: if E.gt(values[id], 0): distribution_total = E.add(distribution_total, values[id]) normalized_distribution[id] = values[id] for id in normalized_distribution: normalized_distribution[id] = E.divide(normalized_distribution[id], distribution_total) return normalized_distribution static func sum_array(values: Array) -> Variant: var result: Variant = 0 for value in values: result = E.add(result, value) return result static func sum(pop: Dictionary) -> Variant: var result: Variant = 0 for n in pop.values(): result = E.add(result, n) return result static var _split_keys: Dictionary = {} ## Split a whole population using normalized float shares. Quantizing the six ## shares to 15 decimal places matches their stored float precision, while the ## integer denominator avoids a second expensive exact normalization pass. static func split_profile(remainders: Dictionary, key: String, count: Variant, ids: Array, shares: Array) -> Dictionary: var result: Dictionary = {} if E.le(count, 0): return result assert(E.eq(count, E.floor_value(count)) and ids.size() == shares.size() and not ids.is_empty()) const SCALE: int = 1000000000000000 var weights: Array = [] var assigned_weight: int = 0 for i in ids.size() - 1: var weight: int = clampi(roundi(clampf(float(shares[i]), 0.0, 1.0) * SCALE), 0, SCALE) weights.append(weight) assigned_weight += weight var last_weight: int = SCALE - assigned_weight if last_weight < 0: var excess: int = -last_weight for i in range(weights.size() - 1, -1, -1): var reduction: int = mini(excess, weights[i]) weights[i] -= reduction excess -= reduction if excess == 0: break last_weight = 0 weights.append(last_weight) var cached: Dictionary = _split_keys.get(key, {}) if cached.is_empty() or cached.ids != ids: var keys: Array = [] for id in ids: keys.append(key + ":" + str(id)) if _split_keys.size() >= 512: _split_keys.clear() cached = {"ids": ids.duplicate(), "keys": keys} _split_keys[key] = cached var quotas: Array = E.whole_quotas(count, weights, SCALE) var rests: Array = [] var order: Array = [] var total: Variant = 0 for i in ids.size(): var fraction: float = quotas[i][1] + E.approx(remainders.get(cached.keys[i], 0)) var adjustment: int = floori(fraction) result[ids[i]] = E.maximum(0, E.add(quotas[i][0], adjustment)) total = E.add(total, result[ids[i]]) rests.append(fraction - adjustment if E.ge(quotas[i][0], -adjustment) else fraction) order.append(i) order.sort_custom(func(a: int, b: int) -> bool: return rests[a] > rests[b] if rests[a] != rests[b] else str(ids[a]) < str(ids[b])) if E.lt(total, count): for i in order: if E.ge(total, count): break result[ids[i]] = E.add(result[ids[i]], 1) rests[i] -= 1.0 total = E.add(total, 1) elif E.gt(total, count): order.reverse() for i in order: if E.le(total, count): break if E.le(result[ids[i]], 0): continue result[ids[i]] = E.subtract(result[ids[i]], 1) rests[i] += 1.0 total = E.subtract(total, 1) for i in ids.size(): remainders[cached.keys[i]] = clampf(rests[i], -1.0, 1.0) return result static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary: if E.le(count, 0): return {} return split_arrays(remainders, key, count, weights.keys(), weights.values()) static func split_arrays(remainders: Dictionary, key: String, count: Variant, ids: Array, weights: Array, population_factor: int = 0, skip_conserved_identity: bool = false) -> Dictionary: var result: Dictionary = {} if E.le(count, 0): return result var sum_weights: Variant = 0 var positive_index: int = -1 var positive_count: int = 0 for i in weights.size(): sum_weights = E.add(sum_weights, weights[i]) if E.gt(weights[i], 0): positive_index = i positive_count += 1 if E.le(sum_weights, 0): return result var cached: Dictionary = _split_keys.get(key, {}) if cached.is_empty() or cached.ids != ids: var keys: Array = [] for id in ids: keys.append(key + ":" + str(id)) if _split_keys.size() >= 512: _split_keys.clear() cached = {"ids": ids.duplicate(), "keys": keys} _split_keys[key] = cached var remainder_keys: Array = cached.keys # A single branch is exact only when no saved rounding error redistributes it. var single: bool = positive_count == 1 and E.eq(count, E.floor_value(count)) if single: for i in ids.size(): if E.lt(weights[i], 0) or E.ne(remainders.get(remainder_keys[i], 0), 0): single = false break if single: for i in ids.size(): result[ids[i]] = count if i == positive_index else 0 remainders[remainder_keys[i]] = 0 return result var expected: Array = [] if population_factor > 0 and E.eq(sum_weights, E.multiply(count, population_factor)): # Ordinary drift: count cancels exactly from count*weight/(factor*count). expected = E.population_quotas(count, weights, population_factor) else: expected = E.mul_div_many(count, weights, sum_weights) var positives: Array = [] var positive_sum: Variant = 0 var whole_quotas: bool = true for i in ids.size(): expected[i] = E.add(expected[i], remainders.get(remainder_keys[i], 0)) var positive: Variant = E.maximum(0, expected[i]) whole_quotas = whole_quotas and E.eq(positive, E.floor_value(positive)) positives.append(positive) positive_sum = E.add(positive_sum, positive) # Fractional identity ratios still round-trip through parts/make in the # original algorithm. Retain them to preserve ties and saved errors exactly. var normalized: Array = positives if E.gt(positive_sum, 0) and (E.ne(positive_sum, count) or (not whole_quotas and not (skip_conserved_identity and E.eq(positive_sum, count)))): normalized = E.mul_div_many(count, positives, positive_sum) var order: Array = [] var rests: Array = [] var assigned: Variant = 0 for i in ids.size(): result[ids[i]] = E.floor_value(normalized[i]) assigned = E.add(assigned, result[ids[i]]) rests.append(E.subtract(normalized[i], result[ids[i]])) order.append(i) order.sort_custom(func(a: int, b: int) -> bool: return E.gt(rests[a], rests[b]) if E.ne(rests[a], rests[b]) else str(ids[a]) < str(ids[b])) for i in order: if E.ge(assigned, count): break result[ids[i]] = E.add(result[ids[i]], 1) assigned = E.add(assigned, 1) for i in ids.size(): remainders[remainder_keys[i]] = E.clamp_value(E.subtract(expected[i], result[ids[i]]), E.neg(1), 1) return result static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant: # Reward rates are floats while populations are exact whole values. Keep # the product as quotient plus fractional carry instead of constructing an # intermediate Exact Value and flooring it again. if probability is float and is_finite(probability) and probability >= 0.0 and (count is int or (count is E.Value and count.fraction == 0.0)) and E.ge(count, 0): var terms: Array = E.parts(probability) if terms[2] > 0: var population: Array = E._blocks(count)[0] var whole_product: Array = E.Big.multiply(population, terms[0]) var qr: Array = E.Big.divmod(whole_product, terms[1]) var fractional: float = E._fraction_ratio(qr[1], terms[1]) var carry: float = E.approx(remainders.get(key, 0)) if carry >= 0.0 and carry < 1.0: if qr[1] == [0]: remainders[key] = carry return E._compose(qr[0]) fractional += carry if fractional > 0.5 and absf(fractional - roundf(fractional)) <= 1e-12: fractional = roundf(fractional) var extra: int = floori(fractional) remainders[key] = fractional - extra return E.add(E._compose(qr[0]), extra) var product: Variant = E.multiply(count, E.maximum(0, probability)) var carry: float = E.approx(remainders.get(key, 0)) if carry >= 0.0 and carry < 1.0 and E.ge(product, 0): if product is int: remainders[key] = carry return product if product is E.Value: var fraction: float = product.fraction + carry if product.fraction > 0.0 and carry > 0.0 and fraction > 0.5 and absf(fraction - roundf(fraction)) <= 1e-12: fraction = roundf(fraction) var extra: int = floori(fraction) remainders[key] = fraction - extra return E.add(E.floor_value(product), extra) var expected: Variant = E.add(product, remainders.get(key, 0)) var actual: Variant = E.floor_value(expected) remainders[key] = E.maximum(0, E.subtract(expected, actual)) return actual