Add new tests and implement frame budget management
- Introduced `civilization_benchmark.gd` and `legacy_quantity_math.gd` for benchmarking and legacy quantity calculations. - Created `test_dynamic_math.gd`, `test_frame_evolution.gd`, and `test_sparse_batches.gd` to validate mathematical operations and frame evolution logic. - Implemented `frame_budget.gd` to manage cooperative work on the main thread, ensuring no partially calculated state is published. - Added UID files for all new scripts to maintain unique identifiers.
This commit is contained in:
@@ -217,11 +217,7 @@ func produce_newborn_lifetime(target: Dictionary, id: String, count: Variant, bo
|
||||
object_years = E.maximum(0, object_years)
|
||||
for resource in products:
|
||||
var coefficient: Variant = E.multiply(products[resource], WorldParameters.production(target, world.catalog.entities[id]))
|
||||
# A giant cohort can accumulate more than E27 object-years. Clamp the
|
||||
# multiplicand before multiplying so the exact-number backend never
|
||||
# constructs an out-of-range intermediate value.
|
||||
var safe_years: Variant = (E.minimum(object_years, E.divide(E.max_value(), coefficient)) if E.gt(coefficient, 0) else 0)
|
||||
var output: Variant = E.multiply(safe_years, coefficient)
|
||||
var output: Variant = E.multiply(object_years, coefficient)
|
||||
if E.contains(["H", "S"], resource):
|
||||
output = E.minimum(output, target.energy)
|
||||
var actual: Variant = world.ledger.credit(target, resource, output)
|
||||
|
||||
@@ -26,7 +26,7 @@ static func rate(target: Dictionary, key: String, count: Variant, probability: V
|
||||
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
||||
# Runtime depends on the number of branches, never the number of civilizations.
|
||||
static func split(target: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||
if (E.le(count, 0) or E.le(QuantityMath.sum(weights), 0)): return {}
|
||||
if E.le(count, 0): return {}
|
||||
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
|
||||
return QuantityMath.split(target.civilization_remainders, key, count, weights)
|
||||
|
||||
@@ -34,11 +34,12 @@ static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictio
|
||||
var weights: Dictionary = {}
|
||||
for ethic in pop:
|
||||
var influence: Variant = (E.of(rows[ethic].diplomacy_rate) if diplomacy else 1)
|
||||
var contribution: Variant = E.multiply(pop[ethic], influence)
|
||||
for other in ETHICS:
|
||||
var weight: Variant = (9 if colony else 1)
|
||||
if E.eq(other, ethic): weight = (960 if colony else 196)
|
||||
elif E.eq(other, OPPOSITE[ethic]): weight = (4 if colony else 0)
|
||||
weights[other] = E.add(weights.get(other, 0), E.multiply(E.multiply(pop[ethic], influence), weight))
|
||||
weights[other] = E.add(weights.get(other, 0), E.multiply(contribution, weight))
|
||||
if E.le(total(weights), 0): return pop.duplicate()
|
||||
return split(target, key, total(pop), weights)
|
||||
|
||||
@@ -48,6 +49,7 @@ static func emit(plan: Dictionary, destination: String, pop: Dictionary) -> void
|
||||
|
||||
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
|
||||
for ethic in pop:
|
||||
if E.le(pop[ethic], 0): continue
|
||||
var weights: Dictionary = {}
|
||||
for term in str(rules[plan.id].ethics[ethic].fall).split("^", false):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
@@ -76,9 +78,11 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop
|
||||
plan.to = E.maximum(plan.to, year)
|
||||
return removed
|
||||
|
||||
static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
|
||||
static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary, physical: Dictionary = {}, budget_timer: RefCounted = null) -> Dictionary:
|
||||
var profile_started: int = world.profile_start()
|
||||
State.reconcile(world, target, rules, physical)
|
||||
var remaining: Dictionary = target.ethics.duplicate(true)
|
||||
var remaining: Dictionary = target.ethics.duplicate()
|
||||
var copied: Dictionary = {}
|
||||
var plans: Dictionary = {}
|
||||
var colonies: Array = []
|
||||
var collateral: Dictionary = {}
|
||||
@@ -88,6 +92,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
||||
var ids: Array = expired.keys()
|
||||
ids.sort()
|
||||
for id in ids:
|
||||
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||
if ((not rules.has(id)) or expired[id].is_empty()): continue
|
||||
var plan: Dictionary = new_plan(id, year)
|
||||
plans[id] = plan
|
||||
@@ -95,6 +100,8 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
||||
plan.amount = E.add(plan.amount, part.amount)
|
||||
plan.from = E.minimum(plan.from, part.from)
|
||||
plan.to = E.maximum(plan.to, part.to)
|
||||
remaining[id] = remaining[id].duplicate()
|
||||
copied[id] = true
|
||||
var triggered: Dictionary = take(world, remaining[id], plan.amount)
|
||||
var successes: Dictionary = {}
|
||||
var failures: Dictionary = {}
|
||||
@@ -113,9 +120,12 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
||||
successes[ethic] = bounded.pro
|
||||
failures[ethic] = bounded.deg
|
||||
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
|
||||
var profile_transitions: int = world.profile_start()
|
||||
add(triggered, progress(world, target, rules, budget, plan, successes))
|
||||
fall(target, plan, rules, failures)
|
||||
world.profile_end("civilization_transitions", profile_transitions)
|
||||
if rules[id].fights:
|
||||
var profile_wars: int = world.profile_start()
|
||||
for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
|
||||
var wars: Variant = E.minimum(total(warriors), E.floor_value(E.divide((E.add(total(triggered), total(remaining[id]))), 2)))
|
||||
warriors = QuantityMath.allocate_whole(warriors, wars)
|
||||
@@ -136,11 +146,15 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
||||
var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide((E.add(E.multiply(outcomes.get("pro50", 0), 10), E.multiply(outcomes.get("pro75", 0), 30))), order), 1)
|
||||
collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage)
|
||||
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars)
|
||||
world.profile_end("wars", profile_wars)
|
||||
emit(plan, id, triggered)
|
||||
if E.gt(total(settlers), 0): colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
|
||||
# Collateral is applied after all stages planned their own attempts.
|
||||
for id in collateral:
|
||||
if (not rules.has(id)): continue
|
||||
if not copied.has(id):
|
||||
remaining[id] = remaining[id].duplicate()
|
||||
copied[id] = true
|
||||
if (not plans.has(id)): plans[id] = new_plan(id, year)
|
||||
var plan: Dictionary = plans[id]
|
||||
var retry: Dictionary = plan.outputs.get(id, {})
|
||||
@@ -153,22 +167,27 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian
|
||||
var incoming: Dictionary = {}
|
||||
var planetary: Dictionary = {}
|
||||
for id in plans:
|
||||
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||
var plan: Dictionary = plans[id]
|
||||
var products: Dictionary = {}
|
||||
for destination in plan.outputs:
|
||||
var pop: Dictionary = plan.outputs[destination]
|
||||
if rules.has(destination):
|
||||
if E.ne(destination, "Ct"):
|
||||
var profile_drift: int = world.profile_start()
|
||||
pop = drift(target, E.add(E.add(E.add(id, ":"), destination), ":drift"), pop, rules[id].ethics, (E.eq(destination, id) and rules[id].diplomacy))
|
||||
world.profile_end("ethics_drift", profile_drift)
|
||||
if (not incoming.has(destination)): incoming[destination] = {}
|
||||
add(incoming[destination], pop)
|
||||
products[destination] = total(pop)
|
||||
if (State.is_space_civilization(id, rules) and E.eq(world.catalog.entities[destination].group_as, "Planet")):
|
||||
planetary[destination] = E.add(planetary.get(destination, 0), total(pop))
|
||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
|
||||
world.profile_end("civilization_prepare", profile_started)
|
||||
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
||||
|
||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary) -> void:
|
||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary, budget_timer: RefCounted = null) -> void:
|
||||
var profile_started: int = world.profile_start()
|
||||
target.ethics = transaction.remaining
|
||||
for id in transaction.incoming:
|
||||
if (not target.ethics.has(id)): target.ethics[id] = {}
|
||||
@@ -184,22 +203,30 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: Var
|
||||
if (not target.binds.has(owner)): target.binds[owner] = {}
|
||||
target.binds[owner][id] = E.add(target.binds[owner].get(id, 0), shares[owner])
|
||||
target.amount[id] = E.subtract(target.amount[id], shares[owner])
|
||||
colonize(world, target, transaction.colonies, year, rules)
|
||||
State.reconcile(world, target, rules)
|
||||
if budget_timer != null:
|
||||
await colonize(world, target, transaction.colonies, year, rules, physical, budget_timer)
|
||||
if budget_timer.cancelled: return
|
||||
else:
|
||||
colonize(world, target, transaction.colonies, year, rules, physical)
|
||||
State.reconcile(world, target, rules, physical if transaction.colonies.is_empty() else {})
|
||||
var worship: Variant = 0
|
||||
for id in rules:
|
||||
if (not rules[id].worships): continue
|
||||
for ethic in target.ethics[id]: worship = E.add(worship, E.multiply(target.ethics[id][ethic], E.of(rules[id].ethics[ethic].worship)))
|
||||
target.civilization_stats.worship = worship
|
||||
world.profile_end("civilization_finish", profile_started)
|
||||
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: Variant, rules: Dictionary) -> void:
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: Variant, rules: Dictionary, physical: Dictionary = {}, budget_timer: RefCounted = null) -> void:
|
||||
if colonies.is_empty(): return
|
||||
var profile_colonies: int = world.profile_start()
|
||||
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
|
||||
var stock: Dictionary = {}
|
||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0)
|
||||
var requests: Dictionary = {}
|
||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
||||
for i in colonies.size():
|
||||
if budget_timer != null and not await budget_timer.pause(): return
|
||||
var colony: Dictionary = colonies[i]
|
||||
var population: Dictionary = QuantityMath.allocate_whole(colony.population, granted[str(i)])
|
||||
population = drift(target, E.add(colony.source, ":colonies"), population, rules[colony.source].ethics, false, true)
|
||||
@@ -225,3 +252,4 @@ static func colonize(world, target: Dictionary, colonies: Array, year: Variant,
|
||||
if (not target.ethics.has(colony.stage)): target.ethics[colony.stage] = {}
|
||||
add(target.ethics[colony.stage], population)
|
||||
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count)
|
||||
world.profile_end("colonization", profile_colonies)
|
||||
|
||||
@@ -44,12 +44,11 @@ static func detach_space_civilizations(target: Dictionary, rules: Dictionary) ->
|
||||
static func stellar_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
|
||||
if physical.is_empty():
|
||||
physical = world.ledger.physical_amounts(target)
|
||||
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
||||
var capacity: Dictionary = {}
|
||||
for id in physical:
|
||||
var entity = world.catalog.entities[id]
|
||||
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
||||
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])
|
||||
for id in layout.star_homes:
|
||||
for home in layout.star_homes[id]:
|
||||
capacity[home] = E.add(capacity.get(home, 0), physical.get(id, 0))
|
||||
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)))
|
||||
@@ -106,22 +105,21 @@ static func admit_transition(budget: Dictionary, rules: Dictionary, source: Stri
|
||||
# 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: Variant) -> void:
|
||||
var profile_started: int = world.profile_start()
|
||||
detach_space_civilizations(target, rules)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
reconcile(world, target, rules, physical)
|
||||
var budget: Dictionary = stellar_budget(world, target, rules, physical)
|
||||
var remaining: Dictionary = budget.capacity.duplicate()
|
||||
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
|
||||
var stages: Array = []
|
||||
for id in rules:
|
||||
if is_space_civilization(id, rules):
|
||||
stages.append(id)
|
||||
stages.sort()
|
||||
var stages: Array = world.catalog.civilization_layout(rules).stages
|
||||
var changed: bool = false
|
||||
for index in range(E.subtract(stages.size(), 1), E.neg(1), E.neg(1)):
|
||||
var id: String = stages[index]
|
||||
var count: Variant = target.amount.get(id, 0)
|
||||
if E.le(count, 0): continue
|
||||
var home: String = rules[id].home
|
||||
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()):
|
||||
@@ -131,6 +129,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
var excess: Variant = E.subtract(count, supported)
|
||||
if E.le(excess, 0):
|
||||
continue
|
||||
changed = true
|
||||
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
|
||||
@@ -161,79 +160,71 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
target.binds[owner] = {}
|
||||
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
|
||||
target.amount[previous] = E.subtract(target.amount[previous], amount)
|
||||
physical = world.ledger.physical_amounts(target)
|
||||
reconcile(world, target, rules, physical)
|
||||
if changed:
|
||||
physical = world.ledger.physical_amounts(target)
|
||||
reconcile(world, target, rules, physical)
|
||||
assign_homes(world, target, rules, physical)
|
||||
world.profile_end("enforce_capacity", profile_started)
|
||||
|
||||
# Unity's structure chain: direct Claim resets the required structure count;
|
||||
# intervening stages require ten times more units of the preceding structure.
|
||||
static func habitat_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
|
||||
if physical.is_empty():
|
||||
physical = world.ledger.physical_amounts(target)
|
||||
var structures: Array = []
|
||||
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
||||
var structures: Array = layout.structures
|
||||
var capacity: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if E.ne(world.catalog.entities[id].claim, ""):
|
||||
structures.append(id)
|
||||
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)
|
||||
stages.sort()
|
||||
var requirements: Dictionary = {}
|
||||
var anchors: Dictionary = {}
|
||||
var anchor: String = ""
|
||||
var units: Variant = 1
|
||||
for id in stages:
|
||||
var direct: String = ""
|
||||
for structure in structures:
|
||||
if E.eq(world.catalog.entities[structure].claim, id): direct = structure
|
||||
if E.ne(direct, ""):
|
||||
anchor = direct
|
||||
units = 1
|
||||
else:
|
||||
units = E.multiply(units, 10)
|
||||
anchors[id] = anchor
|
||||
if (id.begins_with("C3.") or id.begins_with("C4.")):
|
||||
requirements[id] = {"id": anchor, "units": units}
|
||||
for id in structures: capacity[id] = physical.get(id, 0)
|
||||
var requirements: Dictionary = layout.requirements
|
||||
var anchors: Dictionary = layout.anchors
|
||||
var requested: Dictionary = {}
|
||||
for id in requirements:
|
||||
var req: Dictionary = requirements[id]
|
||||
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}
|
||||
|
||||
# A structure's direct reservation belongs only to its Claim stage. Other
|
||||
# stages do not debit it, so each stage's placement can be calculated alone.
|
||||
static func stage_homes(world, target: Dictionary, id: String, layout: Dictionary, capacity: Dictionary = {}) -> Dictionary:
|
||||
var left: Variant = target.amount.get(id, 0)
|
||||
var homes: Dictionary = {}
|
||||
if E.le(left, 0): return homes
|
||||
var allowed: bool = layout.anchors[id] == ""
|
||||
for structure in layout.structures:
|
||||
if structure == layout.anchors[id]: allowed = true
|
||||
if not allowed: continue
|
||||
var stock: Variant = capacity.get(structure, 0) if not capacity.is_empty() else world.ledger.physical_amount(target, structure)
|
||||
if E.le(stock, 0): continue
|
||||
var amount: Variant = left
|
||||
if world.catalog.entities[structure].claim == id:
|
||||
amount = E.minimum(amount, stock)
|
||||
if E.gt(amount, 0): homes[structure] = amount
|
||||
left = E.subtract(left, amount)
|
||||
if E.le(left, 0): break
|
||||
return homes
|
||||
|
||||
static func assign_homes(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void:
|
||||
var budget: Dictionary = habitat_budget(world, target, rules, physical)
|
||||
var used: Dictionary = {}
|
||||
var profile_started: int = world.profile_start()
|
||||
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
||||
var capacity: Dictionary = {}
|
||||
for structure in layout.structures:
|
||||
capacity[structure] = world.ledger.physical_amount(target, structure) if physical.is_empty() else physical.get(structure, 0)
|
||||
target.civilization_homes = {}
|
||||
var stages: Array = budget.anchors.keys()
|
||||
stages.sort()
|
||||
stages.reverse()
|
||||
for id in stages:
|
||||
var left: Variant = target.amount.get(id, 0)
|
||||
var homes: Dictionary = {}
|
||||
var allowed: bool = E.eq(budget.anchors[id], "")
|
||||
for structure in budget.structures:
|
||||
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
|
||||
for index in range(layout.stages.size() - 1, -1, -1):
|
||||
var id: String = layout.stages[index]
|
||||
target.civilization_homes[id] = stage_homes(world, target, id, layout, capacity)
|
||||
world.profile_end("assign_homes", profile_started)
|
||||
|
||||
|
||||
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: Variant = target.amount.get(id, 0)
|
||||
if E.le(population, 0): return base
|
||||
var homes: Dictionary = stage_homes(world, target, id, world.catalog.civilization_layout(rules))
|
||||
target.civilization_homes[id] = homes
|
||||
var weighted: Variant = population
|
||||
for home in target.civilization_homes.get(id, {}):
|
||||
weighted = E.add(weighted, E.multiply(target.civilization_homes[id][home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
||||
for home in homes:
|
||||
weighted = E.add(weighted, E.multiply(homes[home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
||||
return (E.mul_div(base, population, weighted) if E.gt(weighted, 0) else base)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Definitions and their indexes only: no population, batches or UI state.
|
||||
## Definitions are Nodes owned by the caller, as before this extraction.
|
||||
|
||||
@@ -8,7 +9,10 @@ var event_groups: Dictionary[String, Array] = {}
|
||||
var automatic_producers: Array = []
|
||||
var unlockable_entities: Array = []
|
||||
var _group_members: Dictionary = {}
|
||||
var index_revision: int = 0
|
||||
var _indexed_entity_count: int = -1
|
||||
var _civilization_layout: Dictionary = {}
|
||||
var _civilization_rules: Dictionary = {}
|
||||
|
||||
## Populate an empty catalog; replacement uses a new catalog to preserve ownership.
|
||||
func load_definitions(rows: Dictionary) -> void:
|
||||
@@ -28,6 +32,8 @@ func load_definitions(rows: Dictionary) -> void:
|
||||
|
||||
## Call explicitly after modifying group_as without changing the entity count.
|
||||
func rebuild_group_index() -> void:
|
||||
index_revision += 1
|
||||
_civilization_layout.clear()
|
||||
_group_members.clear()
|
||||
for id in entities:
|
||||
var group: String = entities[id].group_as
|
||||
@@ -45,3 +51,47 @@ func is_group(id: String) -> bool:
|
||||
func members(id: String) -> Array:
|
||||
if not is_group(id): return [id]
|
||||
return _group_members.get(id, []).duplicate()
|
||||
|
||||
## Definition-only cache. Rebuild indexes after changing claims or bonuses;
|
||||
## invalidate this layout after changing stage homes in an existing rules table.
|
||||
func invalidate_civilization_layout() -> void:
|
||||
_civilization_layout.clear()
|
||||
|
||||
func civilization_layout(rules: Dictionary) -> Dictionary:
|
||||
_ensure_group_index()
|
||||
if not _civilization_layout.is_empty() and is_same(_civilization_rules, rules):
|
||||
return _civilization_layout
|
||||
_civilization_rules = rules
|
||||
var structures: Array = []
|
||||
for id in entities:
|
||||
if entities[id].claim != "": structures.append(id)
|
||||
structures.sort_custom(func(a, b): return E.gt(entities[a].civ_bonus, entities[b].civ_bonus) if E.ne(entities[a].civ_bonus, entities[b].civ_bonus) else str(a) < str(b))
|
||||
var stages: Array = []
|
||||
for id in rules:
|
||||
if str(rules[id].home) != "": stages.append(id)
|
||||
stages.sort()
|
||||
var anchors: Dictionary = {}
|
||||
var requirements: Dictionary = {}
|
||||
var anchor: String = ""
|
||||
var units: Variant = 1
|
||||
for id in stages:
|
||||
var direct: String = ""
|
||||
for structure in structures:
|
||||
if entities[structure].claim == id: direct = structure
|
||||
if direct != "":
|
||||
anchor = direct
|
||||
units = 1
|
||||
else:
|
||||
units = E.multiply(units, 10)
|
||||
anchors[id] = anchor
|
||||
if id.begins_with("C3.") or id.begins_with("C4."):
|
||||
requirements[id] = {"id": anchor, "units": units}
|
||||
var star_homes: Dictionary = {}
|
||||
for id in entities:
|
||||
var homes: Array = []
|
||||
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
||||
if entities[id].event_group == home or entities[id].count_as == home or id == home:
|
||||
homes.append(home)
|
||||
if not homes.is_empty(): star_homes[id] = homes
|
||||
_civilization_layout = {"structures": structures, "stages": stages, "anchors": anchors, "requirements": requirements, "star_homes": star_homes}
|
||||
return _civilization_layout
|
||||
|
||||
@@ -19,12 +19,20 @@ func initialize_life_batches() -> void:
|
||||
world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime)
|
||||
|
||||
|
||||
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}) -> void:
|
||||
func run_frame(clock: Dictionary, budget: RefCounted) -> void:
|
||||
var _keep_alive: RefCounted = self
|
||||
await evolute_to_datetime(clock.datetime, clock, budget)
|
||||
if not budget.cancelled and world != null: world.finish_frame()
|
||||
|
||||
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefCounted = null) -> void:
|
||||
var _keep_alive: RefCounted = self
|
||||
# Reconcile only when simulation advances.
|
||||
# Fractional frame updates cannot change capacity or populations.
|
||||
var processed_year: Variant = SimulationTime.processed_year(datetime)
|
||||
if E.gt(processed_year, world.state.time.year):
|
||||
var profile_initialize: int = world.profile_start()
|
||||
initialize_life_batches()
|
||||
world.profile_end("initialize", profile_initialize)
|
||||
var previous: Dictionary = world.state
|
||||
var next: Dictionary = previous.duplicate_deep()
|
||||
next.version = SimulationTime.STATE_VERSION
|
||||
@@ -37,11 +45,21 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}) -> void:
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
next.distribution = distribution
|
||||
|
||||
var profile_production: int = world.profile_start()
|
||||
world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year)
|
||||
evolve_batches(next, to_year)
|
||||
world.profile_end("production", profile_production)
|
||||
var profile_generations: int = world.profile_start()
|
||||
if budget != null:
|
||||
await evolve_batches(next, to_year, budget)
|
||||
if budget.cancelled: return
|
||||
else:
|
||||
evolve_batches(next, to_year)
|
||||
world.profile_end("generations", profile_generations)
|
||||
world.ledger.refresh_groups(next)
|
||||
world.ledger.clamp_auto_resources(next)
|
||||
var profile_trends: int = world.profile_start()
|
||||
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
|
||||
world.profile_end("trends", profile_trends)
|
||||
next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year}
|
||||
|
||||
# Publish only after every phase has finished; previous stays untouched.
|
||||
@@ -50,16 +68,19 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}) -> void:
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
func evolve_batches(target: Dictionary, to_year: Variant) -> void:
|
||||
func evolve_batches(target: Dictionary, to_year: Variant, budget: RefCounted = null) -> void:
|
||||
var _keep_alive: RefCounted = self
|
||||
# A long cycle may contain several generations; preserve actual birth intervals.
|
||||
var completed: Dictionary = {}
|
||||
while evolve_batch_generation(target, to_year, completed):
|
||||
pass
|
||||
while await evolve_batch_generation(target, to_year, completed, budget):
|
||||
if budget != null and not await budget.pause(): return
|
||||
|
||||
|
||||
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }) -> bool:
|
||||
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }, budget: RefCounted = null) -> bool:
|
||||
var _keep_alive: RefCounted = self
|
||||
# Apply direct completions at the original next-generation boundary, so
|
||||
# binding transfers, the blast environment and capacity checks keep order.
|
||||
if budget != null and not await budget.pause(): return false
|
||||
var completed_now: Dictionary = completed.duplicate(true)
|
||||
completed.clear()
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||
@@ -92,10 +113,12 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
||||
return false
|
||||
var fractions: Dictionary = {}
|
||||
var outputs: Dictionary = {}
|
||||
var current_quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||
var current_quantities: Dictionary = quantities
|
||||
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities)
|
||||
var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities)
|
||||
var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget)
|
||||
if budget != null and budget.cancelled: return false
|
||||
for id in expired:
|
||||
if budget != null and not await budget.pause(): return false
|
||||
var total: Variant = 0
|
||||
for part : Dictionary in expired[id]:
|
||||
total = E.add(total, part.amount)
|
||||
@@ -127,6 +150,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
||||
for id in expired:
|
||||
for part : Dictionary in expired[id]:
|
||||
for successor in part.products:
|
||||
if budget != null and not await budget.pause(): return false
|
||||
var amount: Variant = part.products[successor]
|
||||
if E.eq(successor, "blast"):
|
||||
blast_power = E.add(blast_power, amount)
|
||||
@@ -137,15 +161,28 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), amount)
|
||||
else:
|
||||
amount = world.ledger.credit(target, successor, amount)
|
||||
var before_batches: Array = target.batches.get(successor, []).duplicate(true)
|
||||
var produces: bool = not world.automatic.yearly_products(world.catalog.entities[successor]).is_empty()
|
||||
var before_batches: Array = target.batches.get(successor, []).duplicate(true) if produces else []
|
||||
var cutoff: Variant = (to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF))
|
||||
var integral: Variant = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
|
||||
var profile_newborn: int = world.profile_start()
|
||||
var integral: Variant
|
||||
if budget != null:
|
||||
integral = await world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff, budget)
|
||||
if budget.cancelled: return false
|
||||
else:
|
||||
integral = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||
if produces:
|
||||
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
|
||||
world.profile_end("newborn_batches", profile_newborn)
|
||||
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
|
||||
world.ledger.accept_transfers(target, incoming)
|
||||
world.ledger.bind_new_owners(target, new_owners)
|
||||
world.ledger.round_object_allocations(target)
|
||||
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
|
||||
if budget != null:
|
||||
await CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules, budget)
|
||||
if budget.cancelled: return false
|
||||
else:
|
||||
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
|
||||
world.stellar.apply_blast(target, blast_power, environment)
|
||||
CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year)
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
extends RefCounted
|
||||
## Cooperative work on the main thread. No partially calculated state is published.
|
||||
var tree: SceneTree
|
||||
var slice_usec: int = 40000
|
||||
var deadline: int
|
||||
var cancelled: bool = false
|
||||
var waiting_usec: int = 0
|
||||
|
||||
func _init(scene_tree: SceneTree, milliseconds: int = 40):
|
||||
tree = scene_tree
|
||||
slice_usec = milliseconds * 1000
|
||||
deadline = Time.get_ticks_usec() + slice_usec
|
||||
|
||||
func pause() -> bool:
|
||||
var keep_alive: RefCounted = self
|
||||
if cancelled: return false
|
||||
if Time.get_ticks_usec() >= deadline:
|
||||
var waiting_from: int = Time.get_ticks_usec()
|
||||
await tree.process_frame
|
||||
keep_alive.waiting_usec += Time.get_ticks_usec() - waiting_from
|
||||
deadline = Time.get_ticks_usec() + slice_usec
|
||||
return not keep_alive.cancelled
|
||||
@@ -0,0 +1 @@
|
||||
uid://hu57pwad02sg
|
||||
@@ -8,7 +8,7 @@ const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
|
||||
static func number(value, digits: int = 0) -> String:
|
||||
if not E.finite(value): return "—"
|
||||
return E.scientific(value, 4) if E.ge(E.absolute(value), 100000) else E.fixed(value, digits)
|
||||
return E.grouped(value, digits, false)
|
||||
|
||||
static func colored(text: String, color: String) -> String:
|
||||
return E.add(E.add(E.add(E.add("[color=", color), "]"), text), "[/color]")
|
||||
|
||||
@@ -1,101 +1,43 @@
|
||||
# Числа симуляции
|
||||
|
||||
`exact.gd` сохраняет единый API `E` для симуляции и интерфейса. Целые числа,
|
||||
помещающиеся в `int64`, остаются обычными `int`. Остальные значения хранятся
|
||||
в `exact_value.gd`:
|
||||
Целые значения в диапазоне int64 хранятся как `int`. Остальные числа имеют вид
|
||||
`polarity × (sum(whole[i] × 10^(9i)) + fraction)`:
|
||||
`whole` — динамический массив целых блоков по основанию 10^9,
|
||||
`fraction` — float от 0 включительно до 1 исключительно. Числа после создания
|
||||
не изменяются. Ограничения E27 в хранении нет; размер массива растёт по необходимости.
|
||||
|
||||
```
|
||||
polarity × (high × 10^14 + low + fraction)
|
||||
```
|
||||
Используйте API `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge` и
|
||||
`E.floor_value/ceil_value/round_value`. Обычные операторы над ExactValue не работают.
|
||||
`E.integer` требует значения в диапазоне int64; `E.approx` явно преобразует
|
||||
число в float и для очень больших чисел возвращает бесконечность.
|
||||
Сравнение через E сохраняет отличие конечного большого числа от INF.
|
||||
|
||||
`high` и `low` — целые блоки от 0 до 10^14−1, `fraction` — `float` от 0 до 1,
|
||||
`polarity` — знак. Значения неизменяемы после создания. Диапазон включает весь
|
||||
порядок E27: модуль целой части меньше 10^28. Каждая единица целой части
|
||||
сохраняется, в том числе при добавлении и вычитании единицы около 10^27.
|
||||
Выход за диапазон проверяется утверждениями; насыщения или обрезания запасов нет.
|
||||
Целые операции выполняются точно. Умножение и деление используют безопасные
|
||||
разряды из `big_integer.gd`; нативная арифметика применяется при отсутствии
|
||||
переполнения int64. `E.mul_div(count, numerator, denominator)` сохраняет точное
|
||||
целое частное, не превращая заранее отношение в приближённый коэффициент.
|
||||
`E.floor_sum` суммирует округлённые линейные квоты без перебора каждого окна.
|
||||
|
||||
Числители и знаменатели в состоянии больше не хранятся. НОД при обычных
|
||||
арифметических операциях не вычисляется; обход состояния для ограничения дробей
|
||||
больше не нужен. `bounded` и `bound_state` оставлены как совместимые пустые операции.
|
||||
Дробный остаток приблизительный: при умножении и делении он преобразуется
|
||||
в десятичную дробь с 16 значащими цифрами. При сложении двух ненулевых остатков
|
||||
перенос около целой границы стабилизируется с допуском 10^-12.
|
||||
Умножение большого количества на float усиливает ошибку коэффициента;
|
||||
для известных точных отношений используйте mul_div. Целые распределения
|
||||
и сохранение числа объектов проверяются строго, без допуска.
|
||||
|
||||
## Операции
|
||||
Возраст сохраняет точную целую часть. Только дробная часть переводится в
|
||||
наносекундные тики, дополнительный остаток времени переносится между кадрами.
|
||||
Синхронный диагностический API evolute отклоняет шаги от 1e27 игровых лет:
|
||||
это защита от непрактичного интервала, а не предел хранимого возраста.
|
||||
Покадровый API ограничивает одну транзакцию обычным шагом 1/60 секунды;
|
||||
остальное запрошенное время остаётся в очереди.
|
||||
|
||||
Сложение, вычитание и сравнение работают непосредственно с блоками.
|
||||
Деление int64 и объединённое умножение/деление при безопасном произведении
|
||||
используют нативную арифметику. Перевод больших целых между блоками и временными
|
||||
разрядами, извлечение коэффициента и сборка результата обходятся без строк.
|
||||
Дробный коэффициент нормализуется численно до 16 значащих цифр; последние биты
|
||||
float могут отличаться от результата прежнего строкового преобразования.
|
||||
Для умножения и деления `big_integer.gd` используется как временный вычислитель
|
||||
с безопасными разрядами по основанию 10^9. Результат сразу возвращается в
|
||||
фиксированную структуру. Размер временных дробей определяется входными float,
|
||||
а не растёт от шага к шагу.
|
||||
Формат сохранения — **5**, версия состояния — **0.3.0**.
|
||||
Большие целые и числа с дробью кодируются как
|
||||
`{"$exact": [[blocks], "float bytes in hex", sign]}`.
|
||||
Байты остатка сохраняются без потери точности JSON. Миграции старых форматов нет.
|
||||
|
||||
`E.mul_div(a, b, denominator)` вычисляет отношение за одну операцию. Это нужно
|
||||
для распределений: произведение двух количеств может превышать E27, хотя итог
|
||||
деления укладывается в диапазон. Промежуточное произведение не становится
|
||||
значением симуляции. Целое частное вычисляется до перевода остатка в float.
|
||||
|
||||
```gdscript
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var stock = E.of("1e27") # Большое целое вводится строкой.
|
||||
stock = E.subtract(stock, 1)
|
||||
var share = E.mul_div(stock, 15, 100) # Точные 15% целого запаса.
|
||||
var whole_share = E.floor_value(share)
|
||||
```
|
||||
|
||||
Использовать `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge`,
|
||||
`E.floor_value/ceil_value/round_value`. Обычные операторы над объектами
|
||||
`ExactValue` не подходят. `E.integer` допускается только для значений int64;
|
||||
`E.approx` — явный переход к приближённому float.
|
||||
|
||||
## Дробная точность
|
||||
|
||||
Дробная часть приближённая. Для временного умножения/деления она преобразуется
|
||||
в десятичную дробь с 16 значащими цифрами; нормализация сохраняет значащие цифры
|
||||
малых коэффициентов. Десятичные стоимости вроде 0.125 и 0.0000000000001
|
||||
сохраняют корректный размер производственной партии. НОД применяется только
|
||||
при определении знаменателя стоимости в `production_step`.
|
||||
|
||||
При сложении двух ненулевых дробных частей перенос около целой границы
|
||||
стабилизируется с допуском 10^-12. Это предотвращает задержку редких событий
|
||||
из-за накопленной ошибки float. Допуск не применяется к целым блокам или
|
||||
округлению частного при делении. Суммы распределённых целых объектов проверяются
|
||||
строго, без допуска.
|
||||
|
||||
При умножении большого количества на приближённый коэффициент ошибка самого
|
||||
коэффициента усиливается: float-доля не обещает точности результата до единицы.
|
||||
Известные точные отношения нужно передавать через `mul_div(count, numerator,
|
||||
denominator)`, не вычисляя коэффициент заранее.
|
||||
|
||||
Возраст хранится как целые годы и дробный остаток; в наносекундные тики
|
||||
переводится только дробная часть года. Полный возраст больше не умножается на
|
||||
10^9. Остаток кадрового времени переносится отдельно. Геометрический объём
|
||||
пустот может превышать E27 из-за множителя 10^22: он вычисляется в float и
|
||||
используется только для приближённых коэффициентов плотности и бластов.
|
||||
|
||||
Форматирование не создаёт больших промежуточных значений симуляции: например,
|
||||
округлённая подпись `1.00E+28` допустима для количества чуть меньше 10^28.
|
||||
|
||||
## Сохранение
|
||||
|
||||
Формат сохранения — **4**, версия состояния — **0.3.0**. Большие целые
|
||||
кодируются как `{"$exact": "целое число"}`. Блоки с дробью кодируются как
|
||||
`{"$fixed": ["high", "low", "float bytes in hex", sign]}`. Восемь байтов float
|
||||
сохраняются без потери точности JSON, включая очень малые остатки.
|
||||
Зашифрованный файл и диагностический JSON используют один снимок.
|
||||
|
||||
Миграции нет. Сохранение прежнего формата при обычной загрузке архивируется
|
||||
существующим механизмом `.incompatible.bak`; начинается новая игра.
|
||||
Тесты не загружают и не перезаписывают рабочее сохранение игрока.
|
||||
|
||||
## Проверки
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||
|
||||
Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python
|
||||
`fractions.Fraction` (160 пар, seed 4321). Произведения находятся внутри E27;
|
||||
дробные результаты сравниваются с абсолютным допуском 10^-12, целые — точно.
|
||||
Дополнительно проверяются границы int64/E27, единичные изменения, точные
|
||||
проценты, объединённое умножение с делением, JSON, дробные рецепты,
|
||||
распределения, жизнь, энергия, время, форматирование и 100 циклов симуляции.
|
||||
Проверки: `test_exact_math.gd`, `test_dynamic_math.gd`, `test_quantity_catalog.gd`,
|
||||
`test_time.gd` и `test_frame_evolution.gd`. Независимый целочисленный эталон
|
||||
покрывает числа до 220 десятичных цифр; отдельные проверки — до E1000,
|
||||
границы int64, доли, сериализацию, распределения и продолжение расчёта.
|
||||
|
||||
@@ -102,3 +102,26 @@ static func gcd(a: Array, b: Array) -> Array:
|
||||
a = b
|
||||
b = remainder
|
||||
return a
|
||||
|
||||
## Sum floor((a*i+b)/m), i=0..n-1, using Euclidean reduction.
|
||||
## All arguments are unsigned; no loop over the number of time windows.
|
||||
static func floor_sum(n: Array, m: Array, a: Array, b: Array) -> Array:
|
||||
var result: Array = [0]
|
||||
while n != [0]:
|
||||
var qr: Array = divmod(a, m)
|
||||
if qr[0] != [0]:
|
||||
var pairs: Array = divmod(multiply(n, subtract(n, [1])), [2])[0]
|
||||
result = add(result, multiply(pairs, qr[0]))
|
||||
a = qr[1]
|
||||
qr = divmod(b, m)
|
||||
result = add(result, multiply(n, qr[0]))
|
||||
b = qr[1]
|
||||
var upper: Array = add(multiply(a, n), b)
|
||||
if compare(upper, m) < 0: break
|
||||
qr = divmod(upper, m)
|
||||
n = qr[0]
|
||||
b = qr[1]
|
||||
var temporary: Array = m
|
||||
m = a
|
||||
a = temporary
|
||||
return result
|
||||
|
||||
+144
-149
@@ -1,27 +1,45 @@
|
||||
extends RefCounted
|
||||
## Fixed whole blocks and a float remainder. Native integers are the fast path.
|
||||
## Dynamic whole blocks and a float remainder. Native integers are the fast path.
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
const Value = preload("res://world/owniverse/math/exact_value.gd")
|
||||
const SMALL: int = 1000000000
|
||||
const MAX_INT: int = 9223372036854775807
|
||||
const MIN_INT: int = -9223372036854775807 - 1
|
||||
const BLOCK: int = 100000000000000 # 10^14
|
||||
const LIMIT: Array = [0, 0, 0, 10] # 10^28, exclusive
|
||||
const MAX_INT_LIMBS: Array = [854775807, 223372036, 9]
|
||||
const MIN_INT_LIMBS: Array = [854775808, 223372036, 9]
|
||||
|
||||
static func max_value():
|
||||
return make([999999999, 999999999, 9999])
|
||||
static func _int_limbs(value: int) -> Array:
|
||||
var result: Array = []
|
||||
while value != 0:
|
||||
result.append(absi(value % SMALL))
|
||||
value /= SMALL
|
||||
return result if not result.is_empty() else [0]
|
||||
|
||||
static func make(n: Array, d: Array = [1], polarity: int = 1):
|
||||
assert(d != [0], "Zero denominator")
|
||||
if n.size() <= 2 and d.size() <= 2:
|
||||
var numerator: int = n[0] + (n[1] * SMALL if n.size() == 2 else 0)
|
||||
var denominator: int = d[0] + (d[1] * SMALL if d.size() == 2 else 0)
|
||||
var whole: int = numerator / denominator
|
||||
var fraction: float = minf(float(numerator % denominator) / float(denominator), 1.0 - pow(2.0, -53))
|
||||
return _compose(whole / BLOCK, whole % BLOCK, fraction, polarity)
|
||||
var qr: Array = Big.divmod(n, d)
|
||||
var fraction: float = _fraction_ratio(qr[1], d)
|
||||
return _from_whole(qr[0], fraction, polarity)
|
||||
static func _compose(whole: Array, fraction: float = 0.0, polarity: int = 1):
|
||||
whole = Big.trim(whole.duplicate())
|
||||
if fraction >= 1.0:
|
||||
var carry: float = floorf(fraction)
|
||||
whole = Big.add(whole, Big.parse("%.0f" % carry))
|
||||
fraction -= carry
|
||||
elif fraction < 0.0:
|
||||
assert(Big.compare(whole, [0]) > 0, "Negative magnitude")
|
||||
whole = Big.subtract(whole, [1])
|
||||
fraction += 1.0
|
||||
if fraction >= 1.0:
|
||||
whole = Big.add(whole, [1])
|
||||
fraction = 0.0
|
||||
if whole == [0] and fraction == 0.0:
|
||||
return 0
|
||||
if fraction == 0.0 and Big.compare(whole, MAX_INT_LIMBS) <= 0:
|
||||
var magnitude: int = int(whole[0]) + (int(whole[1]) * SMALL if whole.size() > 1 else 0) + (int(whole[2]) * 1000000000000000000 if whole.size() > 2 else 0)
|
||||
return magnitude * (-1 if polarity < 0 else 1)
|
||||
if fraction == 0.0 and polarity < 0 and Big.compare(whole, MIN_INT_LIMBS) == 0:
|
||||
return MIN_INT
|
||||
var result = Value.new()
|
||||
result.whole = whole
|
||||
result.fraction = minf(fraction, 1.0 - pow(2.0, -53))
|
||||
result.polarity = -1 if polarity < 0 else 1
|
||||
return result
|
||||
|
||||
static func _fraction_ratio(n: Array, d: Array) -> float:
|
||||
if n == [0]: return 0.0
|
||||
@@ -36,57 +54,15 @@ static func _fraction_ratio(n: Array, d: Array) -> float:
|
||||
fraction *= pow(10.0, mini(0, exponent + 308))
|
||||
return minf(fraction, 1.0 - pow(2.0, -53))
|
||||
|
||||
static func _from_whole(n: Array, fraction: float, polarity: int):
|
||||
assert(Big.compare(n, LIMIT) < 0, "Number exceeds E27 range")
|
||||
var middle: int = n[1] if n.size() > 1 else 0
|
||||
var low: int = n[0] + (middle % 100000) * SMALL
|
||||
var high: int = middle / 100000
|
||||
if n.size() > 2: high += n[2] * 10000
|
||||
if n.size() > 3: high += n[3] * 10000000000000
|
||||
return _compose(high, low, fraction, polarity)
|
||||
|
||||
static func _whole_limbs(high: int, low: int) -> Array:
|
||||
return Big.trim([low % SMALL, low / SMALL + (high % 10000) * 100000,
|
||||
(high / 10000) % SMALL, high / 10000000000000])
|
||||
|
||||
static func _int_limbs(value: int) -> Array:
|
||||
var result: Array = []
|
||||
# Modulo/division before abs also supports MIN_INT.
|
||||
while value != 0:
|
||||
result.append(absi(value % SMALL))
|
||||
value /= SMALL
|
||||
return result if not result.is_empty() else [0]
|
||||
|
||||
static func _compose(high: int, low: int, fraction: float, polarity: int):
|
||||
if fraction >= 1.0:
|
||||
low += int(floorf(fraction))
|
||||
fraction -= floorf(fraction)
|
||||
if fraction < 0.0:
|
||||
low -= 1
|
||||
fraction += 1.0
|
||||
if low >= BLOCK:
|
||||
high += low / BLOCK
|
||||
low %= BLOCK
|
||||
elif low < 0:
|
||||
high -= 1
|
||||
low += BLOCK
|
||||
assert(high >= 0 and high < BLOCK, "Number exceeds E27 range")
|
||||
if fraction == 0.0 and high <= 92233:
|
||||
var prefix: int = high * BLOCK
|
||||
if low <= MAX_INT - prefix: return polarity * (prefix + low)
|
||||
if polarity < 0 and low == MAX_INT - prefix + 1: return MIN_INT
|
||||
var result = Value.new()
|
||||
result.high = high
|
||||
result.low = low
|
||||
result.fraction = fraction
|
||||
result.polarity = polarity
|
||||
return result
|
||||
static func make(n: Array, d: Array = [1], polarity: int = 1):
|
||||
assert(d != [0], "Zero denominator")
|
||||
var qr: Array = Big.divmod(n, d)
|
||||
return _compose(qr[0], _fraction_ratio(qr[1], d), polarity)
|
||||
|
||||
static func _blocks(value) -> Array:
|
||||
value = of(value)
|
||||
if value is Value: return [value.high, value.low, value.fraction, value.polarity]
|
||||
# Signed division also handles MIN_INT without overflowing abs().
|
||||
return [absi(value / BLOCK), absi(value % BLOCK), 0.0, -1 if value < 0 else 1]
|
||||
if value is Value: return [value.whole, value.fraction, value.polarity]
|
||||
return [_int_limbs(value), 0.0, -1 if value < 0 else 1]
|
||||
|
||||
static func of(value):
|
||||
if value is Value or value is int: return value
|
||||
@@ -94,7 +70,7 @@ static func of(value):
|
||||
if not is_finite(value): return value
|
||||
if value == floorf(value) and absf(value) < 1e18: return int(value)
|
||||
var magnitude: float = absf(value)
|
||||
return _from_whole(Big.parse("%.0f" % floorf(magnitude)), magnitude - floorf(magnitude), -1 if value < 0 else 1)
|
||||
return _compose(Big.parse("%.0f" % floorf(magnitude)), magnitude - floorf(magnitude), -1 if value < 0 else 1)
|
||||
var text: String = str(value).strip_edges().to_lower()
|
||||
if text == "": return 0
|
||||
if "/" in text:
|
||||
@@ -114,25 +90,16 @@ static func of(value):
|
||||
return make(Big.parse(text + "0".repeat(maxi(0, power))), Big.parse("1" + "0".repeat(maxi(0, -power))), polarity)
|
||||
|
||||
static func parts(value) -> Array:
|
||||
value = of(value)
|
||||
if value is int: return [_int_limbs(value), [1], -1 if value < 0 else 1]
|
||||
var whole: Array = _whole_limbs(value.high, value.low)
|
||||
if value.fraction == 0.0: return [whole, [1], value.polarity]
|
||||
# Bound temporary decimal work to 16 significant digits of the float.
|
||||
# Normalize first so tiny rates retain their significant digits as well.
|
||||
var exponent: int = maxi(-323, int(floor(log(value.fraction) / log(10.0))))
|
||||
var mantissa: int = roundi((value.fraction / pow(10.0, exponent)) * 1000000000000000.0)
|
||||
var x: Array = _blocks(value)
|
||||
if x[1] == 0.0: return [x[0], [1], x[2]]
|
||||
var exponent: int = maxi(-323, int(floor(log(x[1]) / log(10.0))))
|
||||
var mantissa: int = roundi((x[1] / pow(10.0, exponent)) * 1000000000000000.0)
|
||||
exponent -= 15
|
||||
while mantissa % 10 == 0 and mantissa > 0:
|
||||
mantissa /= 10
|
||||
exponent += 1
|
||||
var n: Array = _int_limbs(mantissa)
|
||||
var d: Array = []
|
||||
d.resize((-exponent) / 9 + 1)
|
||||
d.fill(0)
|
||||
const POWERS: Array = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000]
|
||||
d[d.size() - 1] = POWERS[(-exponent) % 9]
|
||||
return [Big.add(Big.multiply(whole, d), n), d, value.polarity]
|
||||
var denominator: Array = Big.parse("1" + "0".repeat(-exponent))
|
||||
return [Big.add(Big.multiply(x[0], denominator), _int_limbs(mantissa)), denominator, x[2]]
|
||||
|
||||
static func numeric(value) -> bool:
|
||||
return value is int or value is float or value is Value
|
||||
@@ -142,20 +109,28 @@ static func finite(value) -> bool:
|
||||
|
||||
static func approx(value) -> float:
|
||||
if not value is Value: return float(value)
|
||||
return value.polarity * (float(value.high) * BLOCK + float(value.low) + value.fraction)
|
||||
if value.whole == [0]: return value.polarity * value.fraction
|
||||
var start: int = maxi(0, value.whole.size() - 3)
|
||||
var leading: float = 0.0
|
||||
for i in range(value.whole.size() - 1, start - 1, -1): leading = leading * SMALL + value.whole[i]
|
||||
var exponent: int = start * 9
|
||||
if exponent > 308: return INF * value.polarity
|
||||
return value.polarity * (leading * pow(10.0, exponent) + value.fraction)
|
||||
|
||||
static func compare(a, b) -> int:
|
||||
if a is int and b is int: return 0 if a == b else (1 if a > b else -1)
|
||||
if not finite(a) or not finite(b):
|
||||
if finite(a): return -1 if b > 0 else 1
|
||||
if finite(b): return 1 if a > 0 else -1
|
||||
var x: float = approx(a)
|
||||
var y: float = approx(b)
|
||||
return 0 if x == y else (1 if x > y else -1)
|
||||
var x: Array = _blocks(a)
|
||||
var y: Array = _blocks(b)
|
||||
if x[3] != y[3]: return 1 if x[3] > y[3] else -1
|
||||
for i in 3:
|
||||
if x[i] != y[i]: return x[3] * (1 if x[i] > y[i] else -1)
|
||||
return 0
|
||||
if x[2] != y[2]: return 1 if x[2] > y[2] else -1
|
||||
var magnitude: int = Big.compare(x[0], y[0])
|
||||
if magnitude == 0 and x[1] != y[1]: magnitude = 1 if x[1] > y[1] else -1
|
||||
return magnitude * x[2]
|
||||
|
||||
static func eq(a, b) -> bool:
|
||||
if a == null: return b == null
|
||||
@@ -186,7 +161,11 @@ static func ge(a, b) -> bool: return compare(a, b) >= 0 if numeric(a) and numeri
|
||||
static func minimum(a, b): return a if le(a, b) else b
|
||||
static func maximum(a, b): return a if ge(a, b) else b
|
||||
static func clamp_value(a, low, high): return minimum(maximum(a, low), high)
|
||||
static func neg(a): return subtract(0, a)
|
||||
static func neg(a):
|
||||
if not numeric(a): return -a
|
||||
if a is int: return -a if a != MIN_INT else _compose(_int_limbs(a), 0.0, 1)
|
||||
if a is float: return -a
|
||||
return _compose(a.whole, a.fraction, -a.polarity)
|
||||
static func absolute(a): return neg(a) if lt(a, 0) else a
|
||||
|
||||
static func add(a, b):
|
||||
@@ -196,32 +175,24 @@ static func add(a, b):
|
||||
if not finite(a) or not finite(b): return approx(a) + approx(b)
|
||||
var x: Array = _blocks(a)
|
||||
var y: Array = _blocks(b)
|
||||
if x[3] == y[3]:
|
||||
var fraction: float = x[2] + y[2]
|
||||
# Repeated fractional accumulation may land just below an integer.
|
||||
# This tolerance only applies to the fractional carry, never whole blocks.
|
||||
if x[2] > 0.0 and y[2] > 0.0 and fraction > 0.5 and absf(fraction - roundf(fraction)) <= 1e-12:
|
||||
fraction = roundf(fraction)
|
||||
return _compose(x[0] + y[0], x[1] + y[1], fraction, x[3])
|
||||
var order: int = 0
|
||||
for i in 3:
|
||||
if x[i] != y[i]:
|
||||
order = 1 if x[i] > y[i] else -1
|
||||
break
|
||||
if x[2] == y[2]:
|
||||
var fraction: float = x[1] + y[1]
|
||||
if x[1] > 0.0 and y[1] > 0.0 and fraction > 0.5 and absf(fraction - roundf(fraction)) <= 1e-12: fraction = roundf(fraction)
|
||||
return _compose(Big.add(x[0], y[0]), fraction, x[2])
|
||||
var order: int = Big.compare(x[0], y[0])
|
||||
if order == 0 and x[1] != y[1]: order = 1 if x[1] > y[1] else -1
|
||||
if order == 0: return 0
|
||||
if order < 0:
|
||||
var tmp: Array = x
|
||||
var temporary: Array = x
|
||||
x = y
|
||||
y = tmp
|
||||
return _compose(x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3])
|
||||
y = temporary
|
||||
return _compose(Big.subtract(x[0], y[0]), x[1] - y[1], x[2])
|
||||
|
||||
static func subtract(a, b):
|
||||
if not numeric(a) or not numeric(b): return a - b
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a >= MIN_INT + b) or (b < 0 and a <= MAX_INT + b): return a - b
|
||||
if not finite(a) or not finite(b): return approx(a) - approx(b)
|
||||
var y: Array = _blocks(b)
|
||||
return add(a, _compose(y[0], y[1], y[2], -y[3]))
|
||||
return add(a, neg(b))
|
||||
|
||||
static func multiply(a, b):
|
||||
if not numeric(a) or not numeric(b): return a * b
|
||||
@@ -246,6 +217,18 @@ static func mul_div(a, b, divisor):
|
||||
assert(z[0] != [0], "Division by zero")
|
||||
return make(Big.multiply(Big.multiply(x[0], y[0]), z[1]), Big.multiply(Big.multiply(x[1], y[1]), z[0]), x[2] * y[2] * z[2])
|
||||
|
||||
## Sum of nonnegative linear quotas rounded down separately at each boundary.
|
||||
static func floor_sum(count, step, offset, divisor):
|
||||
var n: Array = parts(count)
|
||||
var a: Array = parts(step)
|
||||
var b: Array = parts(offset)
|
||||
var d: Array = parts(divisor)
|
||||
assert(n[1] == [1] and n[2] > 0 and a[2] > 0 and b[2] > 0 and d[2] > 0 and d[0] != [0])
|
||||
var numerator_step: Array = Big.multiply(Big.multiply(a[0], b[1]), d[1])
|
||||
var numerator_offset: Array = Big.multiply(Big.multiply(b[0], a[1]), d[1])
|
||||
var denominator: Array = Big.multiply(Big.multiply(a[1], b[1]), d[0])
|
||||
return make(Big.floor_sum(n[0], denominator, numerator_step, numerator_offset))
|
||||
|
||||
static func divide(a, b):
|
||||
if not numeric(a) or not numeric(b): return a / b
|
||||
if not finite(a) or not finite(b): return approx(a) / approx(b)
|
||||
@@ -257,7 +240,7 @@ static func divide(a, b):
|
||||
var denominator: int = absi(b)
|
||||
var whole: int = numerator / denominator
|
||||
var fraction: float = minf(float(numerator % denominator) / float(denominator), 1.0 - pow(2.0, -53))
|
||||
return _compose(whole / BLOCK, whole % BLOCK, fraction, -1 if (a < 0) != (b < 0) else 1)
|
||||
return _compose(_int_limbs(whole), fraction, -1 if (a < 0) != (b < 0) else 1)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
assert(y[0] != [0], "Division by zero")
|
||||
@@ -266,7 +249,7 @@ static func divide(a, b):
|
||||
static func floor_value(a):
|
||||
if not finite(a) or a is int: return a
|
||||
var x: Array = _blocks(a)
|
||||
return _compose(x[0], x[1] + (1 if x[3] < 0 and x[2] > 0 else 0), 0.0, x[3])
|
||||
return _compose(Big.add(x[0], [1]) if x[2] < 0 and x[1] > 0.0 else x[0], 0.0, x[2])
|
||||
|
||||
static func ceil_value(a): return neg(floor_value(neg(a)))
|
||||
static func round_value(a): return multiply(-1 if lt(a, 0) else 1, floor_value(add(absolute(a), of("0.5"))))
|
||||
@@ -274,8 +257,14 @@ static func remainder(a, b): return subtract(a, multiply(truncate(divide(a, b)),
|
||||
static func truncate(a): return ceil_value(a) if lt(a, 0) else floor_value(a)
|
||||
static func integer(a) -> int:
|
||||
var result = truncate(of(a))
|
||||
assert(result is int, "Value does not fit native int64")
|
||||
return result
|
||||
if result is int: return result
|
||||
if result is Value and result.fraction == 0.0:
|
||||
if result.polarity < 0 and Big.compare(result.whole, MIN_INT_LIMBS) == 0: return MIN_INT
|
||||
if Big.compare(result.whole, MAX_INT_LIMBS) <= 0:
|
||||
var magnitude: int = int(result.whole[0]) + (int(result.whole[1]) * SMALL if result.whole.size() > 1 else 0) + (int(result.whole[2]) * 1000000000000000000 if result.whole.size() > 2 else 0)
|
||||
return magnitude * result.polarity
|
||||
assert(false, "Value does not fit native int64")
|
||||
return 0
|
||||
|
||||
## Kept as no-op API: float remainder storage is already bounded.
|
||||
static func bounded(value): return value
|
||||
@@ -283,15 +272,18 @@ static func bounded(value): return value
|
||||
static func bound_state(value): return value
|
||||
|
||||
static func power(a, b):
|
||||
if finite(b) and lt(b, 0): return of(pow(approx(a), approx(b)))
|
||||
if finite(b) and eq(b, floor_value(b)) and le(absolute(b), 10000):
|
||||
# Small negative powers retain the native float precision; huge bases
|
||||
# need block arithmetic because approx(base) can become infinity.
|
||||
if lt(b, 0) and is_finite(approx(a)):
|
||||
return of(pow(approx(a), approx(b)))
|
||||
var exponent: int = integer(absolute(b))
|
||||
var result = 1
|
||||
while exponent:
|
||||
if exponent % 2: result = multiply(result, a)
|
||||
exponent /= 2
|
||||
if exponent: a = multiply(a, a)
|
||||
return result
|
||||
return divide(1, result) if lt(b, 0) else result
|
||||
return coefficient(pow(approx(a), approx(b)))
|
||||
|
||||
static func coefficient(value: float):
|
||||
@@ -299,8 +291,24 @@ static func coefficient(value: float):
|
||||
if not is_finite(value): return value
|
||||
return of(String.num(value, 12))
|
||||
|
||||
static func square_root(a): return coefficient(sqrt(approx(a)))
|
||||
static func logarithm(a): return coefficient(log(approx(a)))
|
||||
static func square_root(a):
|
||||
var approximate: float = approx(a)
|
||||
if is_finite(approximate): return coefficient(sqrt(approximate))
|
||||
if a is Value and a.polarity > 0:
|
||||
var exponent: int = (Big.decimal(a.whole).length() - 1) / 2
|
||||
var scale = of("1e%d" % exponent)
|
||||
var reduced = divide(a, multiply(scale, scale))
|
||||
return multiply(coefficient(sqrt(approx(reduced))), scale)
|
||||
return coefficient(sqrt(approximate))
|
||||
static func logarithm(a):
|
||||
var approximate: float = approx(a)
|
||||
if is_finite(approximate): return coefficient(log(approximate))
|
||||
if a is Value and a.polarity > 0:
|
||||
var start: int = maxi(0, a.whole.size() - 3)
|
||||
var leading: float = 0.0
|
||||
for i in range(a.whole.size() - 1, start - 1, -1): leading = leading * SMALL + a.whole[i]
|
||||
return coefficient(log(leading) + start * 9 * log(10.0))
|
||||
return coefficient(log(approximate))
|
||||
static func near(a, b) -> bool:
|
||||
return le(absolute(subtract(a, b)), multiply(of("0.00001"), maximum(1, maximum(absolute(a), absolute(b)))))
|
||||
|
||||
@@ -337,9 +345,9 @@ static func scientific(value, digits: int = 4) -> String:
|
||||
exponent += 1
|
||||
return fixed(mantissa, digits) + ("E+%d" if exponent >= 0 else "E%d") % exponent
|
||||
|
||||
static func grouped(value, digits: int = 0) -> String:
|
||||
if eq(value, 0): return ""
|
||||
if ge(absolute(value), 1000000000): return scientific(value, digits)
|
||||
static func grouped(value, digits: int = 0, compact: bool = true) -> String:
|
||||
if eq(value, 0): return "" if compact else fixed(0, digits)
|
||||
if compact and ge(absolute(value), 1000000000): return scientific(value, digits)
|
||||
var text: String = fixed(absolute(value), digits)
|
||||
var parts_text: PackedStringArray = text.split(".")
|
||||
var integer_text: String = parts_text[0]
|
||||
@@ -350,16 +358,15 @@ static func grouped(value, digits: int = 0) -> String:
|
||||
return ("-" if lt(value, 0) else "") + integer_text + result + ("." + parts_text[1] if parts_text.size() > 1 else "")
|
||||
|
||||
static func encode(value):
|
||||
if value is Value:
|
||||
if value is Value or (value is int and (value > 9007199254740991 or value < -9007199254740991)):
|
||||
var blocks: Array = _blocks(value)
|
||||
var bytes := PackedByteArray()
|
||||
bytes.resize(8)
|
||||
bytes.encode_double(0, value.fraction)
|
||||
return {"$fixed": [str(value.high), str(value.low), bytes.hex_encode(), value.polarity]}
|
||||
if value is int and (value > 9007199254740991 or value < -9007199254740991):
|
||||
return {"$exact": str(value)}
|
||||
bytes.encode_double(0, blocks[1])
|
||||
return {"$exact": [blocks[0], bytes.hex_encode(), blocks[2]]}
|
||||
if value is Dictionary:
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = encode(value[k])
|
||||
for key in value: result[key] = encode(value[key])
|
||||
return result
|
||||
if value is Array:
|
||||
var result: Array = []
|
||||
@@ -369,12 +376,11 @@ static func encode(value):
|
||||
|
||||
static func decode(value):
|
||||
if value is Dictionary:
|
||||
if value.size() == 1 and value.has("$fixed"):
|
||||
var fields: Array = value["$fixed"]
|
||||
return _compose(int(fields[0]), int(fields[1]), fields[2].hex_decode().decode_double(0), int(fields[3]))
|
||||
if value.size() == 1 and value.has("$exact"): return of(value["$exact"])
|
||||
if value.size() == 1 and value.has("$exact"):
|
||||
var fields: Array = value["$exact"]
|
||||
return _compose(fields[0], fields[1].hex_decode().decode_double(0), int(fields[2]))
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = decode(value[k])
|
||||
for key in value: result[key] = decode(value[key])
|
||||
return result
|
||||
if value is Array:
|
||||
var result: Array = []
|
||||
@@ -385,30 +391,19 @@ static func decode(value):
|
||||
static func valid_encoded(value) -> bool:
|
||||
if value is float: return is_finite(value) and absf(value) < 1e28
|
||||
if value is Dictionary:
|
||||
if value.has("$fixed"):
|
||||
if value.size() != 1 or not value["$fixed"] is Array: return false
|
||||
var fields: Array = value["$fixed"]
|
||||
if fields.size() != 4: return false
|
||||
for i in 2:
|
||||
if not fields[i] is String or not fields[i].is_valid_int() or fields[i].length() > 14: return false
|
||||
if int(fields[i]) < 0 or int(fields[i]) >= BLOCK: return false
|
||||
if not fields[2] is String or fields[2].length() != 16 or not fields[2].is_valid_hex_number(): return false
|
||||
var fraction: float = fields[2].hex_decode().decode_double(0)
|
||||
return is_finite(fraction) and fraction >= 0.0 and fraction < 1.0 and fields[3] in [-1, 1]
|
||||
if value.has("$fixed"): return false
|
||||
if value.has("$exact"):
|
||||
if value.size() != 1 or not value["$exact"] is String: return false
|
||||
var text: String = value["$exact"]
|
||||
if text.length() > 29: return false
|
||||
var terms: PackedStringArray = text.split("/")
|
||||
if terms.size() != 1: return false
|
||||
var n: String = terms[0].trim_prefix("-")
|
||||
if n.is_empty(): return false
|
||||
for ch in n:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if Big.compare(Big.parse(n), LIMIT) >= 0: return false
|
||||
return true
|
||||
for k in value:
|
||||
if not valid_encoded(value[k]): return false
|
||||
if value.size() != 1 or not value["$exact"] is Array: return false
|
||||
var fields: Array = value["$exact"]
|
||||
if fields.size() != 3 or not fields[0] is Array or not fields[1] is String or not fields[2] in [-1, 1]: return false
|
||||
if fields[0].is_empty() or (fields[0].size() > 1 and fields[0].back() == 0): return false
|
||||
if fields[1].length() != 16 or not fields[1].is_valid_hex_number(): return false
|
||||
for limb in fields[0]:
|
||||
if not limb is int or limb < 0 or limb >= SMALL: return false
|
||||
var fraction: float = fields[1].hex_decode().decode_double(0)
|
||||
return is_finite(fraction) and fraction >= 0.0 and fraction < 1.0
|
||||
for key in value:
|
||||
if not valid_encoded(value[key]): return false
|
||||
if value is Array:
|
||||
for item in value:
|
||||
if not valid_encoded(item): return false
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
extends RefCounted
|
||||
## Immutable magnitude high * 10^14 + low + fraction, with a separate sign.
|
||||
## Immutable signed magnitude: whole is little-endian base 1e9 blocks.
|
||||
var polarity: int = 1
|
||||
var high: int = 0
|
||||
var low: int = 0
|
||||
var whole: Array = [0]
|
||||
var fraction: float = 0.0
|
||||
|
||||
func _to_string() -> String:
|
||||
var whole: String = str(high) + "%014d" % low if high else str(low)
|
||||
var whole_text: String = str(whole.back())
|
||||
for i in range(whole.size() - 2, -1, -1):
|
||||
whole_text += "%09d" % whole[i]
|
||||
var tail: String = String.num(fraction, 16).trim_prefix("0").trim_suffix(".") if fraction else ""
|
||||
return ("-" if polarity < 0 else "") + whole + tail
|
||||
return ("-" if polarity < 0 else "") + whole_text + tail
|
||||
|
||||
@@ -34,7 +34,8 @@ func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
|
||||
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount)
|
||||
|
||||
|
||||
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF) -> Variant:
|
||||
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF, budget: RefCounted = null) -> Variant:
|
||||
var _keep_alive: RefCounted = self
|
||||
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
|
||||
return NAN
|
||||
if E.lt(created_to, created_from):
|
||||
@@ -67,12 +68,46 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
|
||||
window_index = E.add(window_index, 1)
|
||||
var batches_by_start: Dictionary = {}
|
||||
var skip_empty_expired: bool = E.gt(duration, 0) and E.finite(completed_before) and E.eq(amount, E.floor_value(amount))
|
||||
var sparse_windows: bool = E.gt(duration, 0) and E.gt(amount, 0) and E.gt(duration, E.multiply(batch_duration, amount)) and not skip_empty_expired
|
||||
for existing : Dictionary in batches:
|
||||
var start: Variant = existing.get("created_from", E.neg(INF))
|
||||
if (not batches_by_start.has(str(start))):
|
||||
batches_by_start[str(start)] = []
|
||||
batches_by_start[str(start)].append(existing)
|
||||
while true:
|
||||
if budget != null and not await budget.pause(): return NAN
|
||||
# A zero cohort is discarded by withdraw_batches. Jump straight to the
|
||||
# first boundary whose cumulative whole quota increases, rather than
|
||||
# allocating and later removing hundreds of empty dictionaries.
|
||||
if sparse_windows:
|
||||
var threshold: Variant = E.add(created_from, E.mul_div(duration, E.add(E.subtract(amount, remaining_amount), 1), amount))
|
||||
var next_nonempty: Variant = E.subtract(E.ceil_value(E.divide(E.subtract(threshold, origin), batch_duration)), 1)
|
||||
if E.gt(next_nonempty, window_index):
|
||||
window_index = next_nonempty
|
||||
cursor = E.maximum(created_from, E.add(origin, E.multiply(window_index, batch_duration)))
|
||||
if skip_empty_expired:
|
||||
var completed_end_index = E.floor_value(E.divide(E.subtract(E.minimum(created_to, E.subtract(completed_before, lifespan)), origin), batch_duration))
|
||||
var window_count = E.subtract(completed_end_index, window_index)
|
||||
if E.gt(window_count, 1):
|
||||
var first_start = E.add(origin, E.multiply(window_index, batch_duration))
|
||||
var first_end = E.add(first_start, batch_duration)
|
||||
var last_end = E.add(origin, E.multiply(completed_end_index, batch_duration))
|
||||
var last_quota = E.floor_value(E.mul_div(amount, E.subtract(last_end, created_from), duration))
|
||||
var total_completed = E.subtract(last_quota, E.subtract(amount, remaining_amount))
|
||||
# Summation by parts preserves every per-window floor. Its
|
||||
# weighted sum gives the same lifetime production integral.
|
||||
var preceding_quotas = E.floor_sum(E.subtract(window_count, 1), E.multiply(amount, batch_duration), E.multiply(amount, E.subtract(first_end, created_from)), duration)
|
||||
var weighted_windows = E.subtract(E.multiply(E.subtract(window_count, 1), last_quota), preceding_quotas)
|
||||
var first_expired_years = E.subtract(E.subtract(completed_before, E.add(first_start, lifespan)), E.divide(batch_duration, 2))
|
||||
var expired_integral = E.subtract(E.multiply(total_completed, first_expired_years), E.multiply(batch_duration, weighted_windows))
|
||||
remaining_object_years = E.subtract(remaining_object_years, expired_integral)
|
||||
completed_amount = E.add(completed_amount, total_completed)
|
||||
completed_from = E.minimum(completed_from, E.add(first_start, lifespan))
|
||||
completed_to = E.maximum(completed_to, E.add(last_end, lifespan))
|
||||
remaining_amount = E.subtract(remaining_amount, total_completed)
|
||||
window_index = completed_end_index
|
||||
cursor = last_end
|
||||
if E.ge(cursor, created_to): break
|
||||
# Whole allocation changes only when its cumulative quota reaches the
|
||||
# next integer. Skip expired empty windows without changing the span
|
||||
# passed to the next generation or the lifetime production integral.
|
||||
|
||||
@@ -3,7 +3,16 @@ const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
var _amount_snapshot: Dictionary = {}
|
||||
var _binds_snapshot: Dictionary = {}
|
||||
var _physical_cache: Dictionary = {}
|
||||
var _physical_revision: int = -1
|
||||
|
||||
func physical_amounts(source: Dictionary) -> Dictionary:
|
||||
world.catalog._ensure_group_index()
|
||||
if _physical_revision == world.catalog.index_revision and source.amount == _amount_snapshot and source.binds == _binds_snapshot:
|
||||
return _physical_cache.duplicate()
|
||||
var profile_started: int = world.profile_start()
|
||||
var amounts: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if (not world.catalog.is_group(id)):
|
||||
@@ -12,7 +21,12 @@ func physical_amounts(source: Dictionary) -> Dictionary:
|
||||
for id in bindings:
|
||||
if amounts.has(id):
|
||||
amounts[id] = E.add(amounts[id], E.maximum(0, bindings[id]))
|
||||
return amounts
|
||||
_amount_snapshot = source.amount.duplicate()
|
||||
_binds_snapshot = source.binds.duplicate(true)
|
||||
_physical_cache = amounts
|
||||
_physical_revision = world.catalog.index_revision
|
||||
world.profile_end("physical_amounts_build", profile_started)
|
||||
return amounts.duplicate()
|
||||
|
||||
|
||||
func physical_amount(source: Dictionary, id: String) -> Variant:
|
||||
|
||||
@@ -14,6 +14,19 @@ var stellar = preload("res://world/owniverse/stellar_rules.gd").new(self)
|
||||
var life = preload("res://world/owniverse/life_evolution.gd").new(self)
|
||||
var environment = preload("res://world/owniverse/universe_environment.gd").new(self)
|
||||
var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(self)
|
||||
var profile_enabled: bool = false
|
||||
var profile: Dictionary = {}
|
||||
|
||||
func profile_start() -> int:
|
||||
if not profile_enabled: return 0
|
||||
return Time.get_ticks_usec() - (frame_budget.waiting_usec if frame_busy and frame_budget != null else 0)
|
||||
|
||||
func profile_end(section: String, started: int) -> void:
|
||||
if not profile_enabled: return
|
||||
var sample: Array = profile.get(section, [0, 0])
|
||||
sample[0] += profile_start() - started
|
||||
sample[1] += 1
|
||||
profile[section] = sample
|
||||
|
||||
var state: Dictionary = {
|
||||
'version': SimulationTime.STATE_VERSION,
|
||||
@@ -44,10 +57,78 @@ var state: Dictionary = {
|
||||
'stats': {'total_amount': {}},
|
||||
}
|
||||
|
||||
var frame_busy: bool = false
|
||||
var frame_commits: int = 0
|
||||
var frame_clock: Dictionary = {}
|
||||
var frame_budget: RefCounted
|
||||
var _requested_speed: int = 0
|
||||
var _queued_productions: Array = []
|
||||
var frame_active_clock: Dictionary = {}
|
||||
var _resume_clock: Dictionary = {}
|
||||
|
||||
## Keep the requested clock advancing while a transaction spans render frames.
|
||||
func evolute_frame(delta: float, tree: SceneTree) -> void:
|
||||
if not is_finite(delta) or delta < 0.0 or not is_finite(delta * pow(10.0, speed - 1)): return
|
||||
if frame_clock.is_empty(): frame_clock = state.time.duplicate()
|
||||
frame_clock = SimulationTime.advance(frame_clock, delta, speed)
|
||||
if frame_busy: return
|
||||
if E.le(frame_clock.datetime, state.time.datetime) and _resume_clock.is_empty(): return
|
||||
frame_busy = true
|
||||
frame_budget = preload("res://world/owniverse/frame_budget.gd").new(tree)
|
||||
frame_active_clock = _resume_clock if not _resume_clock.is_empty() else frame_clock.duplicate()
|
||||
# Keep each transaction at the normal 60 Hz simulation interval. The
|
||||
# requested clock holds the backlog, rather than feeding an ever-growing
|
||||
# multi-generation interval into one transaction and freezing publication.
|
||||
if _resume_clock.is_empty():
|
||||
var normal_clock: Dictionary = SimulationTime.advance(state.time, 1.0 / 60.0, speed)
|
||||
if E.lt(normal_clock.datetime, frame_clock.datetime):
|
||||
frame_active_clock = normal_clock
|
||||
_resume_clock = {}
|
||||
evolution.run_frame(frame_active_clock, frame_budget)
|
||||
|
||||
func request_production(id: String, amount: Variant, resource: bool = false) -> void:
|
||||
if frame_busy:
|
||||
_queued_productions.append({"id": id, "amount": amount, "resource": resource})
|
||||
elif resource:
|
||||
production.produce_hs(id, amount)
|
||||
else:
|
||||
production.produce(id, amount)
|
||||
|
||||
## Persist work requests, never the partially calculated transaction.
|
||||
func frame_checkpoint() -> Dictionary:
|
||||
return {"requested_clock": frame_clock, "active_clock": frame_active_clock if frame_busy else {}, "speed": speed, "productions": _queued_productions}
|
||||
|
||||
func restore_frame_checkpoint(checkpoint: Dictionary) -> void:
|
||||
frame_clock = checkpoint.get("requested_clock", {})
|
||||
_resume_clock = checkpoint.get("active_clock", {})
|
||||
_queued_productions = checkpoint.get("productions", [])
|
||||
if _resume_clock.is_empty():
|
||||
speed = checkpoint.get("speed", state.speed)
|
||||
else:
|
||||
_requested_speed = checkpoint.get("speed", state.speed)
|
||||
|
||||
func finish_frame() -> void:
|
||||
frame_busy = false
|
||||
frame_commits += 1
|
||||
if _requested_speed != 0:
|
||||
speed = _requested_speed
|
||||
_requested_speed = 0
|
||||
var productions: Array = _queued_productions
|
||||
_queued_productions = []
|
||||
for command in productions:
|
||||
request_production(command.id, command.amount, command.resource)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE and frame_budget != null:
|
||||
frame_budget.cancelled = true
|
||||
|
||||
var speed: int :
|
||||
get:
|
||||
return state.speed
|
||||
return _requested_speed if _requested_speed != 0 else state.speed
|
||||
set(value):
|
||||
if frame_busy:
|
||||
_requested_speed = clampi(value, 1, Global.SPEED_LIMIT)
|
||||
return
|
||||
state.speed = clampi(value, 1, Global.SPEED_LIMIT)
|
||||
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
|
||||
|
||||
|
||||
@@ -12,24 +12,32 @@ static func accumulate_whole(target: Dictionary, field: String, key: String, val
|
||||
|
||||
|
||||
static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||
var total: Variant = 0
|
||||
for amount in requests.values():
|
||||
total = E.add(total, E.floor_value(E.maximum(0, amount)))
|
||||
var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total))
|
||||
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])
|
||||
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 ids: Array = requests.keys()
|
||||
var rests: Array = []
|
||||
var order: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in requests:
|
||||
var expected: Variant = (E.mul_div(E.floor_value(E.maximum(0, requests[id])), available, total) if E.gt(total, 0) else 0)
|
||||
result[id] = E.floor_value(expected)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(expected, result[id])})
|
||||
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for item : Dictionary in order:
|
||||
if E.ge(assigned, available):
|
||||
break
|
||||
if E.lt(result[item.id], E.floor_value(requests[item.id])):
|
||||
result[item.id] = E.add(result[item.id], 1)
|
||||
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
|
||||
|
||||
@@ -74,25 +82,32 @@ static func split(remainders: Dictionary, key: String, count: Variant, weights:
|
||||
var result: Dictionary = {}
|
||||
var sum_weights: Variant = sum(weights)
|
||||
if (E.le(count, 0) or E.le(sum_weights, 0)): return result
|
||||
var expected: Dictionary = {}
|
||||
var ids: Array = weights.keys()
|
||||
var expected: Array = []
|
||||
var remainder_keys: Array = []
|
||||
var positive_sum: Variant = 0
|
||||
for id in weights:
|
||||
expected[id] = E.add(E.mul_div(count, weights[id], sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
|
||||
for id in ids:
|
||||
var remainder_key: String = key + ":" + str(id)
|
||||
remainder_keys.append(remainder_key)
|
||||
var value: Variant = E.add(E.mul_div(count, weights[id], sum_weights), remainders.get(remainder_key, 0))
|
||||
expected.append(value)
|
||||
positive_sum = E.add(positive_sum, E.maximum(0, value))
|
||||
var order: Array = []
|
||||
var rests: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in weights:
|
||||
var normalized: Variant = (E.mul_div(E.maximum(0, expected[id]), count, positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||
result[id] = E.floor_value(normalized)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
|
||||
order.sort_custom(func ( a , b ) : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for entry in order:
|
||||
for i in ids.size():
|
||||
var normalized: Variant = E.mul_div(E.maximum(0, expected[i]), count, positive_sum) if E.gt(positive_sum, 0) else 0
|
||||
result[ids[i]] = E.floor_value(normalized)
|
||||
assigned = E.add(assigned, result[ids[i]])
|
||||
rests.append(E.subtract(normalized, 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[entry.id] = E.add(result[entry.id], 1)
|
||||
result[ids[i]] = E.add(result[ids[i]], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
for id in weights:
|
||||
remainders[E.add(E.add(key, ":"), id)] = E.clamp_value(E.subtract(expected[id], result[id]), E.neg(1), 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
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,18 @@ func stellar_environment(source: Dictionary) -> Dictionary:
|
||||
if E.gt(mass, 0):
|
||||
metallicity = E.maximum(0, E.add(E.multiply(E.of("0.4"), E.logarithm(E.add(E.mul_div(100, source.amount.get("Met", 0), mass), E.of("0.11")))), E.of("0.88")))
|
||||
var base_mass: Variant = E.multiply(65536, ((E.power(mass, E.of("0.1")) if (E.gt(mass, 0) and E.lt(mass, E.of("1.0e10"))) else ((10 if E.ge(mass, E.of("1.0e10")) else 1)))))
|
||||
if not is_finite(volume) or not is_finite(E.approx(E.add(base_mass, mass))):
|
||||
# Dynamic counts may exceed the float exponent range. Evaluate the
|
||||
# same geometry using logarithms, avoiding infinity / infinity.
|
||||
var exact_volume: Variant = quantities.get("S", 0)
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
var size: Variant = E.add(E.maximum(0, entity.space), E.multiply(E.maximum(0, entity.voids), E.of("1e22")))
|
||||
exact_volume = E.add(exact_volume, E.multiply(quantities[id], size))
|
||||
var density_log: float = log(1.23456e-7) + E.approx(E.logarithm(E.add(base_mass, mass))) / 2.0 - E.approx(E.logarithm(E.maximum(1, exact_volume))) / 6.0
|
||||
var density: Variant = E.of("1e%d" % int(floor(density_log / log(10.0))))
|
||||
density = E.multiply(density, E.coefficient(exp(density_log - floor(density_log / log(10.0)) * log(10.0))))
|
||||
return {"metallicity": metallicity, "density": density, "blast": E.clamp_value(E.divide(source.amount.get("S", 0), exact_volume), 0, 1) if E.gt(exact_volume, 0) else 1}
|
||||
return {"metallicity": metallicity, "density": E.coefficient(1.23456e-7 * sqrt(E.approx(E.add(base_mass, mass))) / pow(maxf(1.0, volume), 1.0 / 6.0)), "blast": E.coefficient(clampf(E.approx(source.amount.get("S", 0)) / volume, 0.0, 1.0)) if volume > 0.0 else 1}
|
||||
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@ static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary,
|
||||
for n in population.values(): count = E.add(count, n)
|
||||
if E.gt(count, 0):
|
||||
for ethic in population:
|
||||
var proportion: Variant = E.divide(population[ethic], count)
|
||||
for i in TRENDS.size():
|
||||
rates[i] = E.add(rates[i], E.multiply(E.multiply(E.divide(population[ethic], count), WEIGHTS[ethic][i]), weight))
|
||||
rates[i] = E.add(rates[i], E.multiply(E.multiply(proportion, WEIGHTS[ethic][i]), weight))
|
||||
weight = E.multiply(weight, 2)
|
||||
for i in TRENDS.size():
|
||||
var name: String = TRENDS[i]
|
||||
@@ -84,7 +85,14 @@ static func _integration(target: Dictionary, events: Variant) -> void:
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.seed = 76123
|
||||
if target.civilization_stats.has("trend_rng"):
|
||||
rng.state = E.integer(target.civilization_stats.trend_rng)
|
||||
# Godot exposes the generator state as an unsigned 64-bit value. The
|
||||
# arithmetic layer intentionally stores values above int64 as blocks;
|
||||
# convert its bit pattern back to the signed integer accepted by the
|
||||
# RNG setter after loading.
|
||||
var saved_state: Variant = target.civilization_stats.trend_rng
|
||||
if E.gt(saved_state, 9223372036854775807):
|
||||
saved_state = E.subtract(saved_state, E.of("18446744073709551616"))
|
||||
rng.state = E.integer(saved_state)
|
||||
var counts: Array = [0, 0, 0]
|
||||
if E.le(events, 256):
|
||||
for i in E.integer(events):
|
||||
|
||||
Reference in New Issue
Block a user