Add exact arithmetic implementation and tests

- Introduced exact arithmetic with support for large integers and fractions in `exact.gd`, `big_integer.gd`, and `exact_value.gd`.
- Implemented a comprehensive test suite in `test_exact_math.gd` to validate arithmetic operations against expected results.
- Added a README in `math` directory to document the exact arithmetic system and its usage.
- Ensured that all arithmetic operations maintain precision without floating-point errors, using a custom representation for large numbers.
- Included methods for encoding and decoding exact values for serialization.
This commit is contained in:
Kirill
2026-09-30 08:08:27 +03:00
parent f2111a376a
commit fb8a3f5658
58 changed files with 5139 additions and 2162 deletions
+102 -105
View File
@@ -1,41 +1,42 @@
extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd")
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
const ETHICS = ["m", "l", "w", "d", "c", "b"]
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
return rules.has(id) and str(rules[id].home) != ""
return (rules.has(id) and E.ne(str(rules[id].home), ""))
# Preserve any existing ethics. New populations start equally distributed;
# reductions (including blasts) are proportional and never leave negative counts.
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
if not target.has("ethics"):
if (not target.has("ethics")):
target.ethics = {}
var physical: Dictionary = world.ledger.physical_amounts(target)
for id in rules:
var count: float = physical.get(id, 0.0)
var count: Variant = physical.get(id, 0)
var current: Dictionary = target.ethics.get(id, {})
var total: float = 0.0
var total: Variant = 0
for ethic in ETHICS:
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
total += current[ethic]
if total > count:
current[ethic] = E.maximum(0, current.get(ethic, 0))
total = E.add(total, current[ethic])
if E.gt(total, count):
current = QuantityMath.allocate_whole(current, count)
elif total < count:
var missing: float = count - total
var base: float = floorf(missing / ETHICS.size())
var rest: int = int(missing - base * ETHICS.size())
elif E.lt(total, count):
var missing: Variant = E.subtract(count, total)
var base: Variant = E.floor_value(E.divide(missing, ETHICS.size()))
var rest: int = E.integer(E.subtract(missing, E.multiply(base, ETHICS.size())))
for i in ETHICS.size():
current[ETHICS[i]] += base + (1.0 if i < rest else 0.0)
current[ETHICS[i]] = E.add(current[ETHICS[i]], E.add(base, ((1 if E.lt(i, rest) else 0))))
target.ethics[id] = current
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
for held: Dictionary in target.binds.values():
for held : Dictionary in target.binds.values():
for id in held:
if is_space_civilization(id, rules):
target.amount[id] = target.amount.get(id, 0.0) + held[id]
held[id] = 0.0
target.amount[id] = E.add(target.amount.get(id, 0), held[id])
held[id] = 0
# A shared budget per stellar family, including stars bound into structures.
# Reservations affect capacity, not the physical stock or its production.
@@ -45,65 +46,64 @@ static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dict
for id in physical:
var entity = world.catalog.entities[id]
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
if entity.event_group == home or entity.count_as == home or id == home:
capacity[home] = capacity.get(home, 0.0) + physical[id]
capacity["SMBH"] = maxf(0.0, target.amount.get("SMBH", 0.0))
for held: Dictionary in target.binds.values():
capacity["SMBH"] += maxf(0.0, held.get("SMBH", 0.0))
if ((E.eq(entity.event_group, home) or E.eq(entity.count_as, home)) or E.eq(id, home)):
capacity[home] = E.add(capacity.get(home, 0), physical[id])
capacity["SMBH"] = E.maximum(0, target.amount.get("SMBH", 0))
for held : Dictionary in target.binds.values():
capacity["SMBH"] = E.add(capacity["SMBH"], E.maximum(0, held.get("SMBH", 0)))
var populations: Dictionary = {}
var requested: Dictionary = {}
for id in rules:
if not is_space_civilization(id, rules):
if (not is_space_civilization(id, rules)):
continue
populations[id] = physical.get(id, 0.0)
populations[id] = physical.get(id, 0)
var home: String = rules[id].home
requested[home] = requested.get(home, 0.0) + populations[id] * maxf(1.0, rules[id].stars)
requested[home] = E.add(requested.get(home, 0), E.multiply(populations[id], E.maximum(1, rules[id].stars)))
var free: Dictionary = {}
for home in capacity:
free[home] = maxf(0.0, capacity[home] - requested.get(home, 0.0))
free[home] = E.maximum(0, E.subtract(capacity[home], requested.get(home, 0)))
return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules)}
# Account for the source's existing reservation when changing stages. A failed
# transition retains its population at the source, rather than losing objects.
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String,
destination: String, requested: float) -> float:
if source == destination or not is_space_civilization(destination, rules):
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String, destination: String, requested: Variant) -> Variant:
if (E.eq(source, destination) or (not is_space_civilization(destination, rules))):
return requested
var home: String = rules[destination].home
var cost: float = maxf(1.0, rules[destination].stars)
var cost: Variant = E.maximum(1, rules[destination].stars)
var old_home: String = ""
var old_cost: float = 0.0
var old_cost: Variant = 0
if is_space_civilization(source, rules):
old_home = rules[source].home
old_cost = maxf(1.0, rules[source].stars)
old_cost = E.maximum(1, rules[source].stars)
# Existing overcapacity is handled by the separate population policy.
if budget.requested.get(home, 0.0) > budget.capacity.get(home, 0.0):
return 0.0
var net_cost: float = cost - old_cost if old_home == home else cost
var admitted: float = floorf(maxf(0.0, requested))
if net_cost > 0.0:
admitted = minf(admitted, floorf(budget.free.get(home, 0.0) / net_cost))
if E.gt(budget.requested.get(home, 0), budget.capacity.get(home, 0)):
return 0
var net_cost: Variant = (E.subtract(cost, old_cost) if E.eq(old_home, home) else cost)
var admitted: Variant = E.floor_value(E.maximum(0, requested))
if E.gt(net_cost, 0):
admitted = E.minimum(admitted, E.floor_value(E.divide(budget.free.get(home, 0), net_cost)))
if budget.has("habitats"):
var housing: Dictionary = budget.habitats
var new_req: Dictionary = housing.requirements.get(destination, {})
var old_req: Dictionary = housing.requirements.get(source, {})
if not new_req.is_empty():
var net_units: float = new_req.units - (old_req.units if old_req.get("id", "") == new_req.id else 0.0)
if net_units > 0.0:
var free_units: float = maxf(0.0, housing.capacity.get(new_req.id, 0.0) - housing.requested.get(new_req.id, 0.0))
admitted = minf(admitted, floorf(free_units / net_units))
if not old_req.is_empty(): housing.requested[old_req.id] = maxf(0.0, housing.requested.get(old_req.id, 0.0) - admitted * old_req.units)
if not new_req.is_empty(): housing.requested[new_req.id] = housing.requested.get(new_req.id, 0.0) + admitted * new_req.units
if old_home != "":
budget.requested[old_home] = maxf(0.0, budget.requested.get(old_home, 0.0) - admitted * old_cost)
budget.free[old_home] = maxf(0.0, budget.capacity.get(old_home, 0.0) - budget.requested[old_home])
budget.requested[home] = budget.requested.get(home, 0.0) + admitted * cost
budget.free[home] = maxf(0.0, budget.capacity.get(home, 0.0) - budget.requested[home])
if (not new_req.is_empty()):
var net_units: Variant = E.subtract(new_req.units, ((old_req.units if E.eq(old_req.get("id", ""), new_req.id) else 0)))
if E.gt(net_units, 0):
var free_units: Variant = E.maximum(0, E.subtract(housing.capacity.get(new_req.id, 0), housing.requested.get(new_req.id, 0)))
admitted = E.minimum(admitted, E.floor_value(E.divide(free_units, net_units)))
if (not old_req.is_empty()): housing.requested[old_req.id] = E.maximum(0, E.subtract(housing.requested.get(old_req.id, 0), E.multiply(admitted, old_req.units)))
if (not new_req.is_empty()): housing.requested[new_req.id] = E.add(housing.requested.get(new_req.id, 0), E.multiply(admitted, new_req.units))
if E.ne(old_home, ""):
budget.requested[old_home] = E.maximum(0, E.subtract(budget.requested.get(old_home, 0), E.multiply(admitted, old_cost)))
budget.free[old_home] = E.maximum(0, E.subtract(budget.capacity.get(old_home, 0), budget.requested[old_home]))
budget.requested[home] = E.add(budget.requested.get(home, 0), E.multiply(admitted, cost))
budget.free[home] = E.maximum(0, E.subtract(budget.capacity.get(home, 0), budget.requested[home]))
return admitted
# Resolve shortages from the most advanced stages downwards. This gives existing
# advanced populations priority and lets a demotion cascade in the same cycle.
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: float) -> void:
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void:
detach_space_civilizations(target, rules)
reconcile(world, target, rules)
var budget: Dictionary = stellar_budget(world, target, rules)
@@ -114,50 +114,50 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
if is_space_civilization(id, rules):
stages.append(id)
stages.sort()
for index in range(stages.size() - 1, -1, -1):
for index in range(E.subtract(stages.size(), 1), E.neg(1), E.neg(1)):
var id: String = stages[index]
var home: String = rules[id].home
var cost: float = maxf(1.0, rules[id].stars)
var count: float = target.amount.get(id, 0.0)
var supported: float = minf(count, floorf(remaining.get(home, 0.0) / cost))
var cost: Variant = E.maximum(1, rules[id].stars)
var count: Variant = target.amount.get(id, 0)
var supported: Variant = E.minimum(count, E.floor_value(E.divide(remaining.get(home, 0), cost)))
var requirement: Dictionary = budget.habitats.requirements.get(id, {})
if not requirement.is_empty():
supported = minf(supported, floorf(remaining_homes.get(requirement.id, 0.0) / requirement.units))
remaining_homes[requirement.id] = maxf(0.0, remaining_homes.get(requirement.id, 0.0) - supported * requirement.units)
remaining[home] = maxf(0.0, remaining.get(home, 0.0) - supported * cost)
var excess: float = count - supported
if excess <= 0.0:
if (not requirement.is_empty()):
supported = E.minimum(supported, E.floor_value(E.divide(remaining_homes.get(requirement.id, 0), requirement.units)))
remaining_homes[requirement.id] = E.maximum(0, E.subtract(remaining_homes.get(requirement.id, 0), E.multiply(supported, requirement.units)))
remaining[home] = E.maximum(0, E.subtract(remaining.get(home, 0), E.multiply(supported, cost)))
var excess: Variant = E.subtract(count, supported)
if E.le(excess, 0):
continue
var previous: String = stages[index - 1] if index > 0 else "C1.9"
world.batches.scale_batches(target, id, supported / count)
var previous: String = (stages[E.subtract(index, 1)] if E.gt(index, 0) else "C1.9")
world.batches.scale_batches(target, id, E.divide(supported, count))
target.amount[id] = supported
world.ledger.credit(target, previous, excess)
world.batches.append_batch(target, previous, excess, year, year)
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
if not target.ethics.has(previous):
if (not target.ethics.has(previous)):
target.ethics[previous] = {}
for ethic in ETHICS:
target.ethics[id][ethic] -= removed[ethic]
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
if previous == "C1.9":
target.ethics[id][ethic] = E.subtract(target.ethics[id][ethic], removed[ethic])
target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic])
if E.eq(previous, "C1.9"):
var stars: Dictionary = {}
var physical: Dictionary = world.ledger.physical_amounts(target)
for star in physical:
if world.catalog.entities[star].event_group == "DwStar" and physical[star] > 0.0:
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(physical[star], 0)):
stars[star] = physical[star]
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
# Worlds are aggregates: a star may host several inhabited planets.
var assigned: float = 0.0
var assigned: Variant = 0
var owners: Array = weights.keys()
owners.sort()
for i in owners.size():
var owner: String = owners[i]
var amount: float = floorf(excess * weights[owner]) if i + 1 < owners.size() else excess - assigned
assigned += amount
if not target.binds.has(owner):
var amount: Variant = (E.floor_value(E.multiply(excess, weights[owner])) if E.lt(E.add(i, 1), owners.size()) else E.subtract(excess, assigned))
assigned = E.add(assigned, amount)
if (not target.binds.has(owner)):
target.binds[owner] = {}
target.binds[owner][previous] = target.binds[owner].get(previous, 0.0) + amount
target.amount[previous] -= amount
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
target.amount[previous] = E.subtract(target.amount[previous], amount)
reconcile(world, target, rules)
assign_homes(world, target, rules)
@@ -168,13 +168,10 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
var structures: Array = []
var capacity: Dictionary = {}
for id in world.catalog.entities:
if world.catalog.entities[id].claim != "":
if E.ne(world.catalog.entities[id].claim, ""):
structures.append(id)
capacity[id] = physical.get(id, 0.0)
structures.sort_custom(func(a, b):
var left: float = world.catalog.entities[a].civ_bonus
var right: float = world.catalog.entities[b].civ_bonus
return left > right if left != right else str(a) < str(b))
capacity[id] = physical.get(id, 0)
structures.sort_custom(func ( a , b ) : return (E.gt(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) if E.ne(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) else E.lt(str(a), str(b))))
var stages: Array = []
for id in rules:
if is_space_civilization(id, rules): stages.append(id)
@@ -182,23 +179,23 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
var requirements: Dictionary = {}
var anchors: Dictionary = {}
var anchor: String = ""
var units: float = 1.0
var units: Variant = 1
for id in stages:
var direct: String = ""
for structure in structures:
if world.catalog.entities[structure].claim == id: direct = structure
if direct != "":
if E.eq(world.catalog.entities[structure].claim, id): direct = structure
if E.ne(direct, ""):
anchor = direct
units = 1.0
units = 1
else:
units *= 10.0
units = E.multiply(units, 10)
anchors[id] = anchor
if id.begins_with("C3.") or id.begins_with("C4."):
if (id.begins_with("C3.") or id.begins_with("C4.")):
requirements[id] = {"id": anchor, "units": units}
var requested: Dictionary = {}
for id in requirements:
var req: Dictionary = requirements[id]
requested[req.id] = requested.get(req.id, 0.0) + physical.get(id, 0.0) * req.units
requested[req.id] = E.add(requested.get(req.id, 0), E.multiply(physical.get(id, 0), req.units))
return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors}
static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
@@ -209,29 +206,29 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
stages.sort()
stages.reverse()
for id in stages:
var left: float = target.amount.get(id, 0.0)
var left: Variant = target.amount.get(id, 0)
var homes: Dictionary = {}
var allowed: bool = budget.anchors[id] == ""
var allowed: bool = E.eq(budget.anchors[id], "")
for structure in budget.structures:
if structure == budget.anchors[id]: allowed = true
if not allowed or budget.capacity[structure] <= 0.0: continue
var amount: float = left
if world.catalog.entities[structure].claim == id:
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
used[structure] = used.get(structure, 0.0) + amount
if amount > 0.0: homes[structure] = amount
left -= amount
if left <= 0.0: break
if E.eq(structure, budget.anchors[id]): allowed = true
if ((not allowed) or E.le(budget.capacity[structure], 0)): continue
var amount: Variant = left
if E.eq(world.catalog.entities[structure].claim, id):
amount = E.minimum(amount, E.maximum(0, E.subtract(budget.capacity[structure], used.get(structure, 0))))
used[structure] = E.add(used.get(structure, 0), amount)
if E.gt(amount, 0): homes[structure] = amount
left = E.subtract(left, amount)
if E.le(left, 0): break
target.civilization_homes[id] = homes
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
if not rules.has(id): return world.catalog.entities[id].lifespan
var base: float = rules[id].span
if not is_space_civilization(id, rules): return base
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> Variant:
if (not rules.has(id)): return world.catalog.entities[id].lifespan
var base: Variant = rules[id].span
if (not is_space_civilization(id, rules)): return base
assign_homes(world, target, rules)
var population: float = target.amount.get(id, 0.0)
if population <= 0.0: return base
var weighted: float = population
var population: Variant = target.amount.get(id, 0)
if E.le(population, 0): return base
var weighted: Variant = population
for home in target.civilization_homes.get(id, {}):
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.catalog.entities[home].civ_bonus) - 1.0)
return base * population / weighted if weighted > 0.0 else base
weighted = E.add(weighted, E.multiply(target.civilization_homes[id][home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
return (E.divide(E.multiply(base, population), weighted) if E.gt(weighted, 0) else base)