Add new music tracks to the sound directory
- Added cosmic_unicorns.ogg - Added funky_eldritch.ogg - Added meditative_beings.ogg - Added owniverse_main_theme.ogg These tracks enhance the audio experience of the project.
This commit is contained in:
@@ -6,7 +6,7 @@ const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const SETTING_SECTION = "Settings"
|
||||
const SAVE_PATH = "user://settings.cfg"
|
||||
|
||||
const FORMAT_VERSION : int = 3
|
||||
const FORMAT_VERSION : int = 4
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const STATE_VERSION : String = SimulationTime.STATE_VERSION
|
||||
@@ -179,7 +179,7 @@ func load_data(filename: String, password: String) -> Dictionary:
|
||||
|
||||
|
||||
static func _number(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||
return E.numeric(value) and E.finite(value) and (not whole or E.eq(value, E.floor_value(value))) and (not nonnegative or E.ge(value, 0))
|
||||
return E.numeric(value) and E.finite(value) and (not value is float or absf(value) < 1e28) and (not whole or E.eq(value, E.floor_value(value))) and (not nonnegative or E.ge(value, 0))
|
||||
|
||||
|
||||
static func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||
@@ -249,11 +249,8 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array) ->
|
||||
return "unsupported state.version"
|
||||
if (((not (saved.time is Dictionary)) or (not _number(saved.time.get("datetime")))) or (not _number(saved.time.get("year")))):
|
||||
return "invalid state.time"
|
||||
if ((not E.finite(E.multiply(E.of(saved.time.datetime), 10))) or E.ne(saved.time.year, SimulationTime.processed_year(saved.time.datetime))):
|
||||
if E.ne(saved.time.year, SimulationTime.processed_year(saved.time.datetime)):
|
||||
return "invalid processed time"
|
||||
if saved.time.has("ticks"):
|
||||
if not _number(saved.time.ticks, true) or not E.eq(saved.time.ticks, E.multiply(saved.time.datetime, SimulationTime.TICKS_PER_YEAR)):
|
||||
return "invalid time ticks"
|
||||
if saved.time.has("tick_remainder"):
|
||||
if not E.finite(saved.time.tick_remainder) or E.ge(E.absolute(saved.time.tick_remainder), 1):
|
||||
return "invalid fractional tick"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[gd_resource type="AudioBusLayout" format=3 uid="uid://b5g7p85k5do0m"]
|
||||
|
||||
[resource]
|
||||
bus/1/name = &"Music"
|
||||
bus/1/solo = false
|
||||
bus/1/mute = false
|
||||
bus/1/bypass_fx = false
|
||||
bus/1/volume_db = 0.0
|
||||
bus/1/send = &"Master"
|
||||
bus/2/name = &"FX"
|
||||
bus/2/solo = false
|
||||
bus/2/mute = false
|
||||
bus/2/bypass_fx = false
|
||||
bus/2/volume_db = 0.0
|
||||
bus/2/send = &"Master"
|
||||
+5
-4
@@ -1,9 +1,10 @@
|
||||
# Эволюция звёзд Owniverse.GI
|
||||
|
||||
Числа симуляции переведены на целочисленное хранение с большими разрядами и
|
||||
рациональную арифметику. Количества до `10^27` сохраняют единицы; время хранится
|
||||
в тиках по `10^-9` года. Границы точности коэффициентов, ограничение накопленных
|
||||
дробей и несовместимый со старым формат сохранения 3 описаны в
|
||||
Числа симуляции хранят целую часть в двух блоках int64, дробный остаток — в
|
||||
float. Весь порядок E27 сохраняет целые единицы. Возраст разделён на целые
|
||||
годы и дробную часть с шагом 10^-9 года; большие промежуточные произведения
|
||||
распределений вычисляются через `mul_div`. Границы точности, допуск переноса
|
||||
дробей и несовместимый со старым формат сохранения 4 описаны в
|
||||
[документации математики](world/owniverse/math/README.md).
|
||||
|
||||
Механики перенесены из Owniverse.U с согласованными изменениями GI: постепенные партии, детерминированные дробные остатки, уязвимость цивилизаций до C1.2 и передача выживших планет преемнику. Баланс таблицы GI сохраняется.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
-13
@@ -6,24 +6,25 @@
|
||||
godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json
|
||||
```
|
||||
|
||||
Читает диагностический JSON формата 3 в отдельные экземпляры мира и замеряет
|
||||
Читает диагностический JSON формата 4 в отдельные экземпляры мира и замеряет
|
||||
шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается.
|
||||
Последним аргументом можно задать длительность шага в реальных секундах
|
||||
(по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса.
|
||||
|
||||
При проверке регрессии на копии сохранения с 120 партиями шаг на скорости 6
|
||||
занимал около 1,3 секунды. Главная причина — перебор пустых завершённых окон
|
||||
коротких взрывов; дополнительная — повторное сокращение уже сокращённых дробей.
|
||||
После пропуска таких окон и устранения повторных НОД замеры составили 90–99 мс
|
||||
на шаг 1/60 секунды. Это ускорение примерно в 14 раз, но ещё не бюджет 60 FPS.
|
||||
Размер партии, точность чисел и правила эволюции не изменены. Разреженные
|
||||
сценарии дополнительно сравниваются с полным расчётом в `test_short_lived_events.gd`.
|
||||
На диагностической копии с 120 партиями прежняя рациональная арифметика после
|
||||
оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6.
|
||||
После перехода на целые блоки и float-остаток повторный замер дал 58–62 мс.
|
||||
При шаге 0,1 секунды: 71–82 мс вместо 117–138 мс. Это замеры расчёта на данной
|
||||
копии, а не обещание FPS. Для сравнения старый JSON был преобразован только
|
||||
во временный тестовый файл; в загрузчик игры миграция не добавлена.
|
||||
Разреженные сценарии сравниваются с полным расчётом в `test_short_lived_events.gd`.
|
||||
|
||||
## Точная арифметика
|
||||
## Целые блоки и дробные остатки
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||
|
||||
Независимый эталон Python Fraction, границы int64 и 10^27, точная сериализация,
|
||||
Независимый эталон Python Fraction: 160 пар с произведением внутри E27; целые
|
||||
проверяются точно, дроби — с допуском 10^-12. Также границы int64/E27, сериализация,
|
||||
рецепты с дробными стоимостями, сохранение сумм, время и 100 циклов с меняющимися
|
||||
долями. Все остальные тесты используют числовое сравнение и сериализацию через
|
||||
`E`, а не сравнение ссылок на большие числа. Границы точности и новый формат
|
||||
@@ -90,14 +91,14 @@ godot --headless --path Owniverse.GI --script res://tests/test_time.gd
|
||||
|
||||
Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.
|
||||
|
||||
Схема состояния `0.1.1` хранит:
|
||||
Схема состояния `0.3.0` хранит:
|
||||
|
||||
- `time.datetime` — накопленный возраст, включая необработанную дробную часть;
|
||||
- `time.year` — последний рассчитанный возраст с точностью до десятой доли года.
|
||||
|
||||
Сохранения используют формат 2 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
|
||||
Сохранения используют формат 4 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
|
||||
|
||||
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 2. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
|
||||
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 4. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
|
||||
|
||||
## Согласованные правила эволюции
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ func run():
|
||||
quit(1)
|
||||
return
|
||||
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
|
||||
if not data is Dictionary or data.get("format_version") != 3:
|
||||
push_error("Expected diagnostic JSON format 3")
|
||||
if not data is Dictionary or data.get("format_version") != 4:
|
||||
push_error("Expected diagnostic JSON format 4")
|
||||
quit(1)
|
||||
return
|
||||
var initial: Dictionary = E.decode(restore_json_integers(data)).state
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ func check(value: bool, message: String) -> void:
|
||||
func run() -> void:
|
||||
var actions = load("res://ui/actions/actions.tscn").instantiate()
|
||||
root.add_child(actions)
|
||||
for maximum in [0, E.of("0.1"), E.of("0.99"), 1, E.of("1.9"), 10, 100, 123, 1000, 10000, E.of("1e9"), E.of("1.1e9"), E.of("1e150")]:
|
||||
for maximum in [0, E.of("0.1"), E.of("0.99"), 1, E.of("1.9"), 10, 100, 123, 1000, 10000, E.of("1e9"), E.of("1.1e9"), E.of("9e27")]:
|
||||
actions.set_actions("test", maximum)
|
||||
var labels: Array = []
|
||||
var values: Array = []
|
||||
|
||||
@@ -53,7 +53,7 @@ func invariants(world) -> void:
|
||||
check(E.eq(sum_ethics(world, id), physical.get(id, 0)), E.add("Ethics equal physical population for ", id))
|
||||
for n in world.state.ethics[id].values():
|
||||
if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||
|
||||
func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
|
||||
@@ -59,7 +59,7 @@ func run() -> void:
|
||||
world.state.amount["C0.2"] = 3
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
restored.evolution.initialize_life_batches()
|
||||
@@ -117,7 +117,7 @@ func run() -> void:
|
||||
world.evolution.initialize_life_batches()
|
||||
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
|
||||
+30
-10
@@ -15,20 +15,27 @@ func check(actual, expected: String, label: String):
|
||||
failures += 1
|
||||
push_error(label + ": " + str(actual) + " != " + expected)
|
||||
|
||||
func check_number(actual, expected: String, label: String):
|
||||
var reference = E.of(expected)
|
||||
checks += 1
|
||||
if E.gt(E.absolute(E.subtract(actual, reference)), E.of("1e-12")):
|
||||
failures += 1
|
||||
push_error(label + ": " + str(actual) + " != " + expected)
|
||||
|
||||
func run():
|
||||
# Expected strings were generated independently with Python fractions.Fraction.
|
||||
var cases = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/exact_oracle.json"))
|
||||
for row in cases:
|
||||
var a = E.of(row[0])
|
||||
var b = E.of(row[1])
|
||||
check(E.add(a, b), row[2], "sum")
|
||||
check(E.subtract(a, b), row[3], "difference")
|
||||
check(E.multiply(a, b), row[4], "product")
|
||||
check(E.divide(a, b), row[5], "quotient")
|
||||
check_number(E.add(a, b), row[2], "sum")
|
||||
check_number(E.subtract(a, b), row[3], "difference")
|
||||
check_number(E.multiply(a, b), row[4], "product")
|
||||
check_number(E.divide(a, b), row[5], "quotient")
|
||||
check(E.floor_value(E.divide(a, b)), row[6], "floor")
|
||||
for text in ["9223372036854775807", "-9223372036854775808", "9007199254740993", "1000000000000000000000000001", "-1/10000000000000"]:
|
||||
var value = E.of(text)
|
||||
check(E.decode(JSON.parse_string(JSON.stringify(E.encode(value)))), text, "JSON round trip")
|
||||
check(E.eq(E.decode(JSON.parse_string(JSON.stringify(E.encode(value)))), value), "true", "JSON round trip")
|
||||
check(E.add(E.of("9223372036854775807"), 1), "9223372036854775808", "int64 overflow promoted")
|
||||
check(E.subtract(E.of("-9223372036854775808"), 1), "-9223372036854775809", "int64 underflow promoted")
|
||||
check(Q.production_step([E.of("1e27")]), "1", "large integral recipe")
|
||||
@@ -38,7 +45,7 @@ func run():
|
||||
check(Q.sum(allocation), str(count), "allocation conserves every unit")
|
||||
check(allocation.a, "333333333333333333333333334", "deterministic remainder")
|
||||
var clock = Clock.advance({"datetime": E.of("1e27")}, 0.000000001, 1)
|
||||
check(E.subtract(clock.datetime, E.of("1e27")), "1/1000000000", "single tick at huge age")
|
||||
check_number(E.subtract(clock.datetime, E.of("1e27")), "1/1000000000", "single tick at huge age")
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
for id in world.catalog.entities:
|
||||
@@ -52,10 +59,23 @@ func run():
|
||||
check(world.state.energy, "999999999999999999999999999", "spend one energy")
|
||||
var life = world.life.life_products(world.state, world.catalog.entities.EON5, count)
|
||||
check(Q.sum(life), str(count), "life conservation at 1e27")
|
||||
var precise = E.divide(count, E.of("100000000000000000000000000000000000003"))
|
||||
var bounded = E.bounded(precise)
|
||||
check(E.lt(E.absolute(E.subtract(precise, bounded)), E.of("1e-36")), "true", "bounded fractional error")
|
||||
check(E.bounded(count), str(count), "integer counts never quantized")
|
||||
check(E.mul_div(E.of("1e27"), 15, 100), "150000000000000000000000000", "exact percentage")
|
||||
check(E.mul_div(count, count, count), str(count), "fused product beyond storage range")
|
||||
check(E.subtract(E.add(count, 1), count), "1", "single unit at E27")
|
||||
check(E.floor_value(E.divide(E.subtract(count, 1), count)), "0", "division must not round up a whole object")
|
||||
check(E.fixed(E.add(count, E.of("0.125")), 3), "1000000000000000000000000001.125", "formatting without oversized intermediate")
|
||||
check(E.scientific(E.of("9.999999e27"), 2), "1.00E+28", "display rounding at upper boundary")
|
||||
check(E.eq(E.power(10, -100), E.of("1e-100")), "true", "negative power needs no huge intermediate")
|
||||
var largest = E.of("9999999999999999999999999999")
|
||||
check(E.eq(E.decode(E.encode(largest)), largest), "true", "upper range round trip")
|
||||
check(E.valid_encoded(E.encode(largest)), "true", "upper range encoding valid")
|
||||
for tiny in ["1e-13", "1e-20", "1e-100", "1e-310"]:
|
||||
var value = E.of(tiny)
|
||||
check(E.gt(value, 0), "true", "tiny fraction remains nonzero")
|
||||
check(E.eq(value, E.decode(JSON.parse_string(JSON.stringify(E.encode(value))))), "true", "tiny float round trip")
|
||||
for invalid in [{"$fixed": ["0", "0", "ffffffffffffffff", 1]}, {"$fixed": ["0", "-1", "0000000000000000", 1]}, {"$exact": "1/3"}, {"$exact": "10000000000000000000000000000"}, 1e100]:
|
||||
check(E.valid_encoded(invalid), "false", "reject invalid saved number")
|
||||
world.state.amount.S = E.of("1e24")
|
||||
world.state.amount.H = E.of("1e24")
|
||||
world.state.amount.RD = 1234567
|
||||
@@ -69,7 +89,7 @@ func run():
|
||||
check(str(world.state.auto_star_deficits).length() < 500, "true", "fraction storage stays bounded")
|
||||
print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
|
||||
var saves = root.get_node("SaveManager")
|
||||
check(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
check(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
print("Exact math: %d checks, %d failures" % [checks, failures])
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ func run() -> void:
|
||||
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "Life conserves planets")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
var restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolution.evolve_batches(world.state, 11000000)
|
||||
@@ -118,7 +118,7 @@ func run() -> void:
|
||||
world.evolution.evolve_batches(world.state, 2050)
|
||||
check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host")
|
||||
check(E.eq(world.state.binds.RD["C0.0"], 1000), "C0.1 degradation follows Unity")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
dispose(world)
|
||||
print(("Life: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -43,6 +43,19 @@ func run() -> void:
|
||||
world.catalog.entities.N.bind = {"S": 3}
|
||||
world.state.amount.H = 10
|
||||
world.state.amount.S = 0
|
||||
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), 0), "Default maximum respects missing bindings")
|
||||
world.state.amount.S = 30
|
||||
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), 5), "Default maximum uses recipe resources without an oversized sentinel")
|
||||
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N, 2), 2), "Explicit production limit is preserved")
|
||||
world.state.amount.H = E.of("1e27")
|
||||
world.state.amount.S = E.of("1e27")
|
||||
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), E.floor_value(E.divide(E.of("1e27"), 3))), "Default maximum at E27 retains integer precision")
|
||||
world.state.amount.BH25 = 4
|
||||
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.SMBH), world.production.smbh_available()), "Default SMBH maximum uses available black-hole mass")
|
||||
world.state.amount.BH25 = 0
|
||||
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.SMBH), 0), "Empty SMBH maximum is zero")
|
||||
world.state.amount.H = 10
|
||||
world.state.amount.S = 0
|
||||
world.state.auto_resources = {"H": 10}
|
||||
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
|
||||
@@ -96,7 +96,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.binds.RD.GG, 16) and E.eq(world.state.binds.RD.AB, 120)), "Planet multiplicity applied")
|
||||
check((E.eq(world.state.amount.HP, 0) and E.eq(world.state.stats.total_amount.HP, 1)), "Created planets are bound, counted once")
|
||||
var saves: Node = root.get_node("SaveManager")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
restored = fixture()
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.stellar.create_planets(world.state, "RD", 37, 0, 0, E.of("0.7"))
|
||||
@@ -118,7 +118,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.amount.RP, 0) and E.eq(world.state.amount["C1.3"], 0)), "Inherited planets never become free")
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, width))
|
||||
check((E.eq(world.state.binds.RD["C1.3"], 0) and E.eq(world.state.binds.TWD["C1.3"], 4)), "No planets left at extinct parent type")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
@@ -158,7 +158,7 @@ func run() -> void:
|
||||
check((E.eq(world.state.amount.SN, 0) and E.eq(world.state.amount.LNS, 1)), "Supernova produces a remnant")
|
||||
check(((E.eq(world.state.amount.H, 8) and E.eq(world.state.amount.He, 50)) and E.eq(world.state.amount.Met, 11)), "Supernova returns table resources")
|
||||
check((not world.state.amount.has("blast")), "Supernova dispatches blast output")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
|
||||
+3
-3
@@ -48,13 +48,13 @@ func run() -> void:
|
||||
world.evolute(E.of("0.2"))
|
||||
check((E.eq(world.state.time.year, E.of("0.3")) and E.eq(world.state.amount.H, 3)), "Do not reprocess previous tenths")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
for invalid in [0, E.neg(1), NAN, INF, E.of("1e308")]:
|
||||
for invalid in [0, E.neg(1), NAN, INF, 1e308]:
|
||||
world.evolute(invalid)
|
||||
check(E.eq(world.state, before), "Invalid/overflow interval rejected")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
check(E.eq(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
var invalid_save: Dictionary = world.state.duplicate(true)
|
||||
invalid_save.time.year = E.of("0.25")
|
||||
check(E.ne(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
check(E.ne(saves.validate_data({"format_version": 4, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
dispose(world)
|
||||
|
||||
for fps in [30, 60, 144]:
|
||||
|
||||
@@ -133,7 +133,7 @@ func run() -> void:
|
||||
Parameters.advance(source, world.state, rules, E.of("1.0e12"))
|
||||
check((E.ge(world.state.trends.Integration, 0) and E.lt(world.state.trends.Integration, 1)), "Very large intervals finish with a valid trend fraction")
|
||||
check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale")
|
||||
var payload: Dictionary = {"format_version": 3, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var payload: Dictionary = {"format_version": 4, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var manager: Node = root.get_node("SaveManager")
|
||||
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
||||
var old: Dictionary = payload.duplicate(true)
|
||||
|
||||
@@ -43,6 +43,7 @@ func _get_distribution(value: Variant) -> Array:
|
||||
while E.lt(power, value):
|
||||
distribution.append(power)
|
||||
if E.gt(distribution.size(), 4): distribution.pop_front()
|
||||
if E.ge(power, E.of("1e27")): break
|
||||
power = E.multiply(power, 10)
|
||||
distribution.append(value)
|
||||
while E.lt(distribution.size(), 5): distribution.append(0)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
|
||||
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
|
||||
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Старые поправки `snappedf` удалены: количества считаются целочисленно, дробные вычисления выполняет [модуль точной арифметики](math/README.md). Там же описаны границы точности и формат сохранения 3.
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Количества считаются целочисленно в диапазоне E27, дробные остатки хранятся в `float`. [Модуль арифметики](math/README.md) описывает допуск дробных переносов, точные отношения и формат сохранения 4.
|
||||
|
||||
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
var automatic: Dictionary = {}
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
var products: Dictionary = yearly_products(entity)
|
||||
if products.is_empty(): continue
|
||||
var object_years: Variant = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||
var auto_share: Variant = E.clamp_value(entity.auto, 0, 1)
|
||||
var products: Dictionary = yearly_products(entity)
|
||||
for product in products:
|
||||
var amount: Variant = E.multiply(E.multiply(object_years, products[product]), WorldParameters.production(previous, entity))
|
||||
basic[product] = E.add(basic.get(product, 0), E.multiply(amount, (E.subtract(1, auto_share))))
|
||||
@@ -146,7 +147,7 @@ func produce_brown_dwarfs(target: Dictionary, new_hydrogen: Variant, from_year:
|
||||
var cost: Variant = entity.cost.get("H", 0)
|
||||
if E.le(cost, 0):
|
||||
return
|
||||
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.divide(E.multiply(E.maximum(0, new_hydrogen), E.of("0.001")), cost))
|
||||
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.mul_div(E.maximum(0, new_hydrogen), E.of("0.001"), cost))
|
||||
var fuel: Variant = E.maximum(0, target.auto_star_deficits.get("HBD_fuel", 0))
|
||||
var available: Variant = E.minimum(target.auto_resources.get("H", 0), world.ledger.available(target, "H"))
|
||||
quota = E.minimum(quota, E.divide((E.add(available, fuel)), cost))
|
||||
|
||||
@@ -231,4 +231,4 @@ static func effective_span(world, target: Dictionary, id: String, rules: Diction
|
||||
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))))
|
||||
return (E.divide(E.multiply(base, population), weighted) if E.gt(weighted, 0) else base)
|
||||
return (E.mul_div(base, population, weighted) if E.gt(weighted, 0) else base)
|
||||
|
||||
@@ -66,7 +66,7 @@ static func variables(world, global: Node, id: String, rate: Variant = 0.0) -> D
|
||||
var h: Variant = world.state.amount.get("H", 0)
|
||||
var mixture: String = "—"
|
||||
if E.gt(E.add(he, h), 0):
|
||||
mixture = E.add(E.add(E.add(E.add("He: ", number(E.divide(E.multiply(100, he), (E.add(he, h))), 2)), "% / H: "), number(E.divide(E.multiply(100, h), (E.add(he, h))), 2)), "%")
|
||||
mixture = E.add(E.add(E.add(E.add("He: ", number(E.mul_div(100, he, (E.add(he, h))), 2)), "% / H: "), number(E.mul_div(100, h, (E.add(he, h))), 2)), "%")
|
||||
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||
return {"amount": amount, "colored costs": costs(world, global, id), "lifespan": (number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—"), "voids": number(voids), "he / h": E.add("\n", mixture), "metallicity": E.add(number(E.multiply(environment.metallicity, 100), 2), "%"), "энергия": number(world.state.energy), "плотность": number(environment.density, 6), "лет в секунду": number(world.state.speed_in_year), "amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—"}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant)
|
||||
target.life_remainders = {}
|
||||
for channel in ["progress", "degradation"]:
|
||||
var key: String = E.add(E.add(entity.id, ":"), channel)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(count, probabilities[channel]), probability_sum), target.life_remainders.get(key, 0))
|
||||
var expected: Variant = E.add(E.mul_div(count, probabilities[channel], probability_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
pending[channel] = E.floor_value(expected)
|
||||
var assigned: Dictionary = QuantityMath.allocate_whole(pending, count)
|
||||
@@ -31,7 +31,7 @@ func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant)
|
||||
var requests: Dictionary = {}
|
||||
for id in weights:
|
||||
var key: String = E.add(E.add(entity.id, ":fall:"), id)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(assigned.degradation, weights[id]), weight_sum), target.life_remainders.get(key, 0))
|
||||
var expected: Variant = E.add(E.mul_div(assigned.degradation, weights[id], weight_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
requests[id] = E.maximum(0, E.floor_value(expected))
|
||||
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
|
||||
|
||||
@@ -74,7 +74,7 @@ func plan_smbh(desired_amount: Variant) -> Dictionary:
|
||||
var order: Array = []
|
||||
var consumed_mass: Variant = 0
|
||||
for id in distribution:
|
||||
var expected: Variant = E.divide(E.multiply(requested, distribution[id]), world.catalog.entities[id].mass)
|
||||
var expected: Variant = E.mul_div(requested, distribution[id], world.catalog.entities[id].mass)
|
||||
taken[id] = E.minimum(E.floor_value(expected), E.floor_value(world.state.amount[id]))
|
||||
consumed_mass = E.add(consumed_mass, E.multiply(taken[id], world.catalog.entities[id].mass))
|
||||
order.append({"id": id, "remainder": E.subtract(expected, taken[id])})
|
||||
@@ -173,9 +173,9 @@ func production_step(entity: OwniverseEntity) -> Variant:
|
||||
return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values()))
|
||||
|
||||
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = 1.0E150) -> Variant:
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = null) -> Variant:
|
||||
if E.eq(entity.id, "SMBH"):
|
||||
return plan_smbh(desired_amount).amount
|
||||
return plan_smbh(smbh_available() if desired_amount == null else desired_amount).amount
|
||||
var requirements: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
requirements[resource] = E.maximum(0, entity.cost[resource])
|
||||
@@ -189,7 +189,7 @@ func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant =
|
||||
# No recipe means unavailable for production, not unlimited free objects.
|
||||
if (not has_requirements):
|
||||
return 0
|
||||
var available: Variant = E.maximum(0, desired_amount)
|
||||
var available: Variant = INF if desired_amount == null else E.maximum(0, desired_amount)
|
||||
for resource in requirements:
|
||||
if E.gt(requirements[resource], 0):
|
||||
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource])))
|
||||
|
||||
@@ -1,82 +1,96 @@
|
||||
# Числа симуляции
|
||||
|
||||
Количество объектов, запасы, энергия, расходы, привязки и статистика считаются
|
||||
целочисленно, без перехода через `float`. Значения до `10^27` сохраняют каждую
|
||||
единицу; промежуточные произведения могут превышать этот диапазон.
|
||||
`exact.gd` сохраняет единый API `E` для симуляции и интерфейса. Целые числа,
|
||||
помещающиеся в `int64`, остаются обычными `int`. Остальные значения хранятся
|
||||
в `exact_value.gd`:
|
||||
|
||||
`big_integer.gd` хранит модуль числа массивом разрядов по основанию `10^9`,
|
||||
младший разряд первым. Переносы при умножении не переполняют `int64`.
|
||||
Деление использует нормализованную оценку цифры частного.
|
||||
```
|
||||
polarity × (high × 10^14 + low + fraction)
|
||||
```
|
||||
|
||||
`exact.gd` — единый API арифметики. Малые значения остаются обычными `int`,
|
||||
большие числа и дроби представлены `exact_value.gd`: знак, целый числитель,
|
||||
целый знаменатель. Дроби сокращаются. Операции создают новые значения;
|
||||
поля `ExactValue` и массивы его разрядов нельзя изменять после создания.
|
||||
`high` и `low` — целые блоки от 0 до 10^14−1, `fraction` — `float` от 0 до 1,
|
||||
`polarity` — знак. Значения неизменяемы после создания. Диапазон включает весь
|
||||
порядок E27: модуль целой части меньше 10^28. Каждая единица целой части
|
||||
сохраняется, в том числе при добавлении и вычитании единицы около 10^27.
|
||||
Выход за диапазон проверяется утверждениями; насыщения или обрезания запасов нет.
|
||||
|
||||
Числители и знаменатели в состоянии больше не хранятся. НОД при обычных
|
||||
арифметических операциях не вычисляется; обход состояния для ограничения дробей
|
||||
больше не нужен. `bounded` и `bound_state` оставлены как совместимые пустые операции.
|
||||
|
||||
## Операции
|
||||
|
||||
Сложение, вычитание и сравнение работают непосредственно с блоками.
|
||||
Для умножения и деления `big_integer.gd` используется как временный вычислитель
|
||||
с безопасными разрядами по основанию 10^9. Результат сразу возвращается в
|
||||
фиксированную структуру. Размер временных дробей определяется входными float,
|
||||
а не растёт от шага к шагу.
|
||||
|
||||
`E.mul_div(a, b, denominator)` вычисляет отношение за одну операцию. Это нужно
|
||||
для распределений: произведение двух количеств может превышать E27, хотя итог
|
||||
деления укладывается в диапазон. Промежуточное произведение не становится
|
||||
значением симуляции. Целое частное вычисляется до перевода остатка в float.
|
||||
|
||||
```gdscript
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var stock = E.of("1e27") # Строка, не литерал float 1e27!
|
||||
stock = E.subtract(stock, 1) # 999999999999999999999999999
|
||||
var price = E.of("0.125") # Точно 1/8
|
||||
var cost = E.multiply(price, 8) # Точно 1
|
||||
if E.ge(stock, cost):
|
||||
stock = E.subtract(stock, cost)
|
||||
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`. Перегрузки операторов в GDScript нет:
|
||||
обычные `+`, `==`, `min`, `float(...)` для чисел симуляции не подходят.
|
||||
Сигнатуры принимают `Variant`, а не `float`. `E.integer` допускается только
|
||||
для значений в диапазоне `int64`, например индексов и состояния генератора.
|
||||
Для поиска чисел в массиве используется `E.contains`, поскольку сравнение
|
||||
объектов по ссылке не является сравнением числовых значений.
|
||||
`E.floor_value/ceil_value/round_value`. Обычные операторы над объектами
|
||||
`ExactValue` не подходят. `E.integer` допускается только для значений int64;
|
||||
`E.approx` — явный переход к приближённому float.
|
||||
|
||||
## Границы точности
|
||||
## Дробная точность
|
||||
|
||||
- Десятичные параметры CSV разбираются из строк точно. Стоимости, проверка
|
||||
доступности и размер целой производственной партии не округляются.
|
||||
- Суммы распределений и переносы целых объектов сохраняются точно.
|
||||
- Промежуточная рациональная арифметика точна. Перед публикацией следующего
|
||||
состояния знаменатели сохранённых дробей ограничиваются `10^36`.
|
||||
Если знаменатель больше, значение усекается к нулю с ошибкой меньше
|
||||
`10^-36`. Целые числа эта операция не меняет. Это предотвращает рост
|
||||
знаменателей при многократном накоплении меняющихся долей. Такая точность
|
||||
относится к каждой операции ограничения, а не является обещанием нулевой
|
||||
суммарной ошибки при бесконечном числе циклов.
|
||||
- Логарифмы, корни, нецелые степени и нормальная случайная выборка остаются
|
||||
приближёнными. Результаты трансцендентных функций переводятся обратно в
|
||||
рациональные коэффициенты с 12 знаками после точки. Статистическая выборка
|
||||
не обеспечивает равномерный выбор каждого отдельного целого около `10^27`;
|
||||
последующий учёт выбранного количества и остатка точен.
|
||||
- Возраст хранится в целых тиках (`10^9` тиков на год). Малый остаток кадрового
|
||||
времени остаётся `float` и переносится между кадрами; большой возраст
|
||||
никогда не складывается с малым приращением через `float`.
|
||||
- Интерфейс преобразует в `float` только координаты индикаторов. Текст чисел,
|
||||
доступное количество и значения кнопок формируются из точных чисел.
|
||||
Дробная часть приближённая. Для временного умножения/деления она преобразуется
|
||||
в десятичную дробь с 16 значащими цифрами; нормализация сохраняет значащие цифры
|
||||
малых коэффициентов. Десятичные стоимости вроде 0.125 и 0.0000000000001
|
||||
сохраняют корректный размер производственной партии. НОД применяется только
|
||||
при определении знаменателя стоимости в `production_step`.
|
||||
|
||||
`E.of(float)` сохраняет фактическое двоичное значение входного числа: он
|
||||
не может восстановить единицы, уже потерянные предыдущими вычислениями.
|
||||
При сложении двух ненулевых дробных частей перенос около целой границы
|
||||
стабилизируется с допуском 10^-12. Это предотвращает задержку редких событий
|
||||
из-за накопленной ошибки float. Допуск не применяется к целым блокам или
|
||||
округлению частного при делении. Суммы распределённых целых объектов проверяются
|
||||
строго, без допуска.
|
||||
|
||||
При умножении большого количества на приближённый коэффициент ошибка самого
|
||||
коэффициента усиливается: float-доля не обещает точности результата до единицы.
|
||||
Известные точные отношения нужно передавать через `mul_div(count, numerator,
|
||||
denominator)`, не вычисляя коэффициент заранее.
|
||||
|
||||
Возраст хранится как целые годы и дробный остаток; в наносекундные тики
|
||||
переводится только дробная часть года. Полный возраст больше не умножается на
|
||||
10^9. Остаток кадрового времени переносится отдельно. Геометрический объём
|
||||
пустот может превышать E27 из-за множителя 10^22: он вычисляется в float и
|
||||
используется только для приближённых коэффициентов плотности и бластов.
|
||||
|
||||
Форматирование не создаёт больших промежуточных значений симуляции: например,
|
||||
округлённая подпись `1.00E+28` допустима для количества чуть меньше 10^28.
|
||||
|
||||
## Сохранение
|
||||
|
||||
Формат сохранения — **3**, версия состояния — **0.2.0**. На главном потоке
|
||||
`E.encode` строит независимый снимок, затем существующий фоновый писатель
|
||||
сохраняет его. Большие целые и дроби кодируются как `{"$exact": "число"}`;
|
||||
для дробей используется строка `числитель/знаменатель`. Целые за пределами
|
||||
точного диапазона JSON/float тоже кодируются строкой. Зашифрованный файл и
|
||||
диагностический JSON используют один снимок. После проверки `E.decode`
|
||||
восстанавливает числа, без загрузки сериализованных объектов GDScript.
|
||||
Формат сохранения — **4**, версия состояния — **0.3.0**. Большие целые
|
||||
кодируются как `{"$exact": "целое число"}`. Блоки с дробью кодируются как
|
||||
`{"$fixed": ["high", "low", "float bytes in hex", sign]}`. Восемь байтов float
|
||||
сохраняются без потери точности JSON, включая очень малые остатки.
|
||||
Зашифрованный файл и диагностический JSON используют один снимок.
|
||||
|
||||
Миграции старой арифметики нет. Старый формат несовместим: существующий
|
||||
механизм загрузки сохраняет его в `.incompatible.bak` и начинает новую игру.
|
||||
Старое сохранение игрока не используется тестами и не изменялось при разработке.
|
||||
Миграции нет. Сохранение прежнего формата при обычной загрузке архивируется
|
||||
существующим механизмом `.incompatible.bak`; начинается новая игра.
|
||||
Тесты не загружают и не перезаписывают рабочее сохранение игрока.
|
||||
|
||||
## Проверки
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||
|
||||
Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python
|
||||
`fractions.Fraction` (120 пар, seed 4321). Тест сравнивает строковые результаты
|
||||
суммы, разности, произведения, деления и округления вниз. Дополнительно
|
||||
проверяются границы `int64`, JSON, рецепты, распределения, жизнь при `10^27`,
|
||||
единичный расход энергии, тик времени и 100 циклов с меняющимися долями звёзд.
|
||||
`fractions.Fraction` (160 пар, seed 4321). Произведения находятся внутри E27;
|
||||
дробные результаты сравниваются с абсолютным допуском 10^-12, целые — точно.
|
||||
Дополнительно проверяются границы int64/E27, единичные изменения, точные
|
||||
проценты, объединённое умножение с делением, JSON, дробные рецепты,
|
||||
распределения, жизнь, энергия, время, форматирование и 100 циклов симуляции.
|
||||
|
||||
+152
-114
@@ -1,56 +1,76 @@
|
||||
extends RefCounted
|
||||
## Exact signed rational arithmetic. Native small integers are the fast path.
|
||||
## Fixed 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 CARRY_SCALE: Array = [0, 0, 0, 0, 1] # 10^36
|
||||
const BLOCK: int = 100000000000000 # 10^14
|
||||
const LIMIT: Array = [0, 0, 0, 10] # 10^28, exclusive
|
||||
|
||||
static func make(n: Array, d: Array = [1], polarity: int = 1):
|
||||
assert(d != [0], "Zero denominator")
|
||||
if n == [0]: return 0
|
||||
if d != [1]:
|
||||
var common: Array = Big.gcd(n, d)
|
||||
n = Big.divmod(n, common)[0]
|
||||
d = Big.divmod(d, common)[0]
|
||||
return _make_reduced(n, d, polarity)
|
||||
var qr: Array = Big.divmod(n, d)
|
||||
var fraction: float = 0.0
|
||||
if qr[1] != [0]:
|
||||
var numerator_text: String = Big.decimal(qr[1])
|
||||
var denominator_text: String = Big.decimal(d)
|
||||
if denominator_text.length() < 300:
|
||||
fraction = float(numerator_text) / float(denominator_text)
|
||||
else:
|
||||
var exponent: int = maxi(0, numerator_text.length() - 16) - maxi(0, denominator_text.length() - 16)
|
||||
fraction = float(numerator_text.left(16)) / float(denominator_text.left(16))
|
||||
fraction *= pow(10.0, maxi(-308, exponent))
|
||||
fraction *= pow(10.0, mini(0, exponent + 308))
|
||||
# A remainder below one must not round up and create a whole object.
|
||||
fraction = minf(fraction, 1.0 - pow(2.0, -53))
|
||||
return _from_whole(qr[0], fraction, polarity)
|
||||
|
||||
## Internal: operands already reduced; do not repeat Euclid's algorithm.
|
||||
static func _make_reduced(n: Array, d: Array, polarity: int):
|
||||
if n == [0]: return 0
|
||||
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)):
|
||||
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0))
|
||||
if d == [1] and n.size() == 3:
|
||||
var limit: Array = [854775807 if polarity > 0 else 854775808, 223372036, 9]
|
||||
if Big.compare(n, limit) <= 0:
|
||||
return (("-" if polarity < 0 else "") + Big.decimal(n)).to_int()
|
||||
static func _from_whole(n: Array, fraction: float, polarity: int):
|
||||
var text: String = Big.decimal(n)
|
||||
assert(Big.compare(n, LIMIT) < 0, "Number exceeds E27 range")
|
||||
var high: int = int(text.left(text.length() - 14)) if text.length() > 14 else 0
|
||||
var low: int = int(text.right(14))
|
||||
return _compose(high, low, fraction, polarity)
|
||||
|
||||
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.numerator = n
|
||||
result.denominator = d
|
||||
result.high = high
|
||||
result.low = low
|
||||
result.fraction = fraction
|
||||
result.polarity = polarity
|
||||
return result
|
||||
|
||||
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]
|
||||
|
||||
static func of(value):
|
||||
if value is Value or value is int: return value
|
||||
if value is float:
|
||||
if not is_finite(value): return value
|
||||
if value == floorf(value) and absf(value) < 1e18: return int(value)
|
||||
# Imports the actual binary float, not an invented decimal rounding.
|
||||
var data := PackedByteArray()
|
||||
data.resize(8)
|
||||
data.encode_double(0, value)
|
||||
var bits: int = data.decode_s64(0)
|
||||
var exponent: int = (bits >> 52) & 2047
|
||||
var mantissa: int = bits & 4503599627370495
|
||||
if exponent: mantissa += 4503599627370496
|
||||
var power: int = exponent - 1075 if exponent else -1074
|
||||
var n: Array = Big.parse(str(mantissa))
|
||||
var d: Array = [1]
|
||||
for i in absi(power):
|
||||
if power > 0: n = Big.multiply(n, [2])
|
||||
else: d = Big.multiply(d, [2])
|
||||
return make(n, d, -1 if value < 0 else 1)
|
||||
var magnitude: float = absf(value)
|
||||
return _from_whole(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:
|
||||
@@ -72,7 +92,17 @@ static func of(value):
|
||||
static func parts(value) -> Array:
|
||||
value = of(value)
|
||||
if value is int: return [Big.parse(str(value).trim_prefix("-")), [1], -1 if value < 0 else 1]
|
||||
return [value.numerator, value.denominator, value.polarity]
|
||||
var whole: Array = Big.parse(str(value.high) + "%014d" % 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 text: String = String.num(value.fraction / pow(10.0, exponent), 15)
|
||||
var point: int = text.find(".")
|
||||
if point >= 0: exponent -= text.length() - point - 1
|
||||
var n: Array = Big.parse(text.replace(".", ""))
|
||||
var d: Array = Big.parse("1" + "0".repeat(-exponent))
|
||||
return [Big.add(Big.multiply(whole, d), n), d, value.polarity]
|
||||
|
||||
static func numeric(value) -> bool:
|
||||
return value is int or value is float or value is Value
|
||||
@@ -82,9 +112,7 @@ static func finite(value) -> bool:
|
||||
|
||||
static func approx(value) -> float:
|
||||
if not value is Value: return float(value)
|
||||
var a: String = Big.decimal(value.numerator)
|
||||
var b: String = Big.decimal(value.denominator)
|
||||
return value.polarity * float(a.left(16)) / float(b.left(16)) * pow(10.0, maxi(0, a.length() - 16) - maxi(0, b.length() - 16))
|
||||
return value.polarity * (float(value.high) * BLOCK + float(value.low) + 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)
|
||||
@@ -92,10 +120,12 @@ static func compare(a, b) -> int:
|
||||
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 = parts(a)
|
||||
var y: Array = parts(b)
|
||||
if x[2] != y[2]: return 1 if x[2] > y[2] else -1
|
||||
return x[2] * Big.compare(Big.multiply(x[0], y[1]), Big.multiply(y[0], x[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
|
||||
|
||||
static func eq(a, b) -> bool:
|
||||
if a == null: return b == null
|
||||
@@ -134,69 +164,68 @@ static func add(a, b):
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a <= MAX_INT - b) or (b < 0 and a >= MIN_INT - b): return a + b
|
||||
if not finite(a) or not finite(b): return approx(a) + approx(b)
|
||||
if a is int and a == 0: return of(b)
|
||||
if b is int and b == 0: return of(a)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var common: Array = Big.gcd(x[1], y[1])
|
||||
var xd: Array = Big.divmod(x[1], common)[0]
|
||||
var yd: Array = Big.divmod(y[1], common)[0]
|
||||
var left: Array = Big.multiply(x[0], yd)
|
||||
var right: Array = Big.multiply(y[0], xd)
|
||||
var denominator: Array = Big.multiply(xd, y[1])
|
||||
var numerator: Array
|
||||
var polarity: int = x[2]
|
||||
if x[2] == y[2]:
|
||||
numerator = Big.add(left, right)
|
||||
else:
|
||||
var order: int = Big.compare(left, right)
|
||||
numerator = Big.subtract(left, right) if order >= 0 else Big.subtract(right, left)
|
||||
polarity = x[2] if order >= 0 else y[2]
|
||||
# Reduced inputs can only share a remaining factor from gcd(d1, d2).
|
||||
# Testing the entire new denominator repeats expensive long division.
|
||||
var reduction: Array = Big.gcd(numerator, common)
|
||||
return _make_reduced(Big.divmod(numerator, reduction)[0], Big.divmod(denominator, reduction)[0], polarity)
|
||||
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 order == 0: return 0
|
||||
if order < 0:
|
||||
var tmp: Array = x
|
||||
x = y
|
||||
y = tmp
|
||||
return _compose(x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3])
|
||||
|
||||
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)
|
||||
if b is int and b == 0: return of(a)
|
||||
var y: Array = parts(b)
|
||||
return add(a, _make_reduced(y[0], y[1], -y[2]))
|
||||
var y: Array = _blocks(b)
|
||||
return add(a, _compose(y[0], y[1], y[2], -y[3]))
|
||||
|
||||
static func multiply(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)
|
||||
if (a is int and a == 0) or (b is int and b == 0): return 0
|
||||
if a is int and a == 1: return of(b)
|
||||
if b is int and b == 1: return of(a)
|
||||
if eq(a, 0) or eq(b, 0): return 0
|
||||
if eq(a, 1): return of(b)
|
||||
if eq(b, 1): return of(a)
|
||||
if a is int and b is int and a != MIN_INT and b != MIN_INT:
|
||||
if absi(a) <= MAX_INT / absi(b): return a * b
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var g1: Array = Big.gcd(x[0], y[1])
|
||||
var g2: Array = Big.gcd(y[0], x[1])
|
||||
return _make_reduced(Big.multiply(Big.divmod(x[0], g1)[0], Big.divmod(y[0], g2)[0]), Big.multiply(Big.divmod(x[1], g2)[0], Big.divmod(y[1], g1)[0]), x[2] * y[2])
|
||||
return make(Big.multiply(x[0], y[0]), Big.multiply(x[1], y[1]), x[2] * y[2])
|
||||
|
||||
## Fused ratio: the temporary product is never stored as a simulation value.
|
||||
static func mul_div(a, b, divisor):
|
||||
if not finite(a) or not finite(b) or not finite(divisor): return of(approx(a) * approx(b) / approx(divisor))
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var z: Array = parts(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])
|
||||
|
||||
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)
|
||||
if b is int and b == 1: return of(a)
|
||||
if eq(b, 1): return of(a)
|
||||
if a is int and b is int and b != 0 and not (a == MIN_INT and b == -1):
|
||||
if a % b == 0: return a / b
|
||||
var y: Array = parts(b)
|
||||
assert(y[0] != [0], "Exact division by zero")
|
||||
return multiply(a, _make_reduced(y[1], y[0], y[2]))
|
||||
return mul_div(a, 1, b)
|
||||
|
||||
static func floor_value(a):
|
||||
if not finite(a): return a
|
||||
if a is int: return a
|
||||
var x: Array = parts(a)
|
||||
var qr: Array = Big.divmod(x[0], x[1])
|
||||
if x[2] < 0 and qr[1] != [0]: qr[0] = Big.add(qr[0], [1])
|
||||
return make(qr[0], [1], x[2])
|
||||
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])
|
||||
|
||||
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"))))
|
||||
@@ -207,24 +236,13 @@ static func integer(a) -> int:
|
||||
assert(result is int, "Value does not fit native int64")
|
||||
return result
|
||||
|
||||
## Bound persistent fractions, never integer quantities or recipe arithmetic.
|
||||
## Truncation towards zero changes a value by less than 10^-36.
|
||||
static func bounded(value):
|
||||
if not value is Value or Big.compare(value.denominator, CARRY_SCALE) <= 0:
|
||||
return value
|
||||
var units: Array = Big.divmod(Big.multiply(value.numerator, CARRY_SCALE), value.denominator)[0]
|
||||
return make(units, CARRY_SCALE, value.polarity)
|
||||
## Kept as no-op API: float remainder storage is already bounded.
|
||||
static func bounded(value): return value
|
||||
|
||||
## Only call on a newly owned state snapshot, never on a published old state.
|
||||
static func bound_state(value):
|
||||
if value is Value: return bounded(value)
|
||||
if value is Dictionary:
|
||||
for key in value: value[key] = bound_state(value[key])
|
||||
elif value is Array:
|
||||
for i in value.size(): value[i] = bound_state(value[i])
|
||||
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):
|
||||
var exponent: int = integer(absolute(b))
|
||||
var result = 1
|
||||
@@ -232,7 +250,7 @@ static func power(a, b):
|
||||
if exponent % 2: result = multiply(result, a)
|
||||
exponent /= 2
|
||||
if exponent: a = multiply(a, a)
|
||||
return divide(1, result) if lt(b, 0) else result
|
||||
return result
|
||||
return coefficient(pow(approx(a), approx(b)))
|
||||
|
||||
static func coefficient(value: float):
|
||||
@@ -253,21 +271,27 @@ static func contains(container, value) -> bool:
|
||||
return value in container
|
||||
|
||||
static func fixed(value, digits: int = 0) -> String:
|
||||
var scale = power(10, digits)
|
||||
var rounded = round_value(multiply(value, scale))
|
||||
var text: String = str(absolute(rounded))
|
||||
if not finite(value): return str(value)
|
||||
var x: Array = parts(value)
|
||||
var scale: Array = Big.parse("1" + "0".repeat(digits))
|
||||
var qr: Array = Big.divmod(Big.multiply(x[0], scale), x[1])
|
||||
if Big.compare(Big.multiply(qr[1], [2]), x[1]) >= 0: qr[0] = Big.add(qr[0], [1])
|
||||
var text: String = Big.decimal(qr[0])
|
||||
if digits:
|
||||
text = text.lpad(digits + 1, "0")
|
||||
text = text.left(text.length() - digits) + "." + text.right(digits)
|
||||
return ("-" if lt(rounded, 0) else "") + text
|
||||
return ("-" if x[2] < 0 and qr[0] != [0] else "") + text
|
||||
|
||||
static func scientific(value, digits: int = 4) -> String:
|
||||
if eq(value, 0): return fixed(0, digits) + "E+0"
|
||||
var terms: Array = parts(absolute(value))
|
||||
var exponent: int = Big.decimal(terms[0]).length() - Big.decimal(terms[1]).length()
|
||||
if lt(absolute(value), power(10, exponent)): exponent -= 1
|
||||
var mantissa = divide(value, power(10, exponent))
|
||||
if ge(absolute(round_value(multiply(mantissa, power(10, digits)))), multiply(10, power(10, digits))):
|
||||
var scale = of("1e%d" % exponent)
|
||||
if lt(absolute(value), scale):
|
||||
exponent -= 1
|
||||
scale = of("1e%d" % exponent)
|
||||
var mantissa = divide(value, scale)
|
||||
if float(fixed(absolute(mantissa), digits)) >= 10.0:
|
||||
mantissa = divide(mantissa, 10)
|
||||
exponent += 1
|
||||
return fixed(mantissa, digits) + ("E+%d" if exponent >= 0 else "E%d") % exponent
|
||||
@@ -285,7 +309,11 @@ 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: return {"$exact": str(value)}
|
||||
if value is 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)}
|
||||
if value is Dictionary:
|
||||
@@ -300,6 +328,9 @@ 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"])
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = decode(value[k])
|
||||
@@ -311,22 +342,29 @@ static func decode(value):
|
||||
return 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("$exact"):
|
||||
if value.size() != 1 or not value["$exact"] is String: return false
|
||||
var text: String = value["$exact"]
|
||||
if text.length() > 4096: return false
|
||||
if text.length() > 29: return false
|
||||
var terms: PackedStringArray = text.split("/")
|
||||
if terms.size() > 2: return false
|
||||
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 terms.size() == 2:
|
||||
if terms[1].is_empty(): return false
|
||||
for ch in terms[1]:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if Big.parse(terms[1]) == [0]: 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
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
extends RefCounted
|
||||
## Immutable by contract; arithmetic creates new values. Signed reduced fraction.
|
||||
## Immutable magnitude high * 10^14 + low + fraction, with a separate sign.
|
||||
var polarity: int = 1
|
||||
var numerator: Array = [0]
|
||||
var denominator: Array = [1]
|
||||
var high: int = 0
|
||||
var low: int = 0
|
||||
var fraction: float = 0.0
|
||||
|
||||
func _to_string() -> String:
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
return ("-" if polarity < 0 else "") + Big.decimal(numerator) + ("/" + Big.decimal(denominator) if denominator != [1] else "")
|
||||
var whole: String = str(high) + "%014d" % low if high else str(low)
|
||||
var tail: String = String.num(fraction, 16).trim_prefix("0").trim_suffix(".") if fraction else ""
|
||||
return ("-" if polarity < 0 else "") + whole + tail
|
||||
|
||||
@@ -66,7 +66,7 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
|
||||
# next integer. Skip expired empty windows without changing the span
|
||||
# passed to the next generation or the lifetime production integral.
|
||||
if skip_empty_expired:
|
||||
var threshold = E.add(created_from, E.divide(E.multiply(duration, E.add(E.subtract(amount, remaining_amount), 1)), amount))
|
||||
var threshold = E.add(created_from, E.mul_div(duration, E.add(E.subtract(amount, remaining_amount), 1), amount))
|
||||
var next_nonempty = E.subtract(E.ceil_value(E.divide(E.subtract(threshold, origin), batch_duration)), 1)
|
||||
var expired_end = E.floor_value(E.divide(E.subtract(E.subtract(completed_before, lifespan), origin), batch_duration))
|
||||
var skip_to = E.minimum(next_nonempty, expired_end)
|
||||
@@ -83,7 +83,7 @@ func append_batch(target: Dictionary, id: String, amount: Variant, created_from:
|
||||
var part_end: Variant = E.minimum(created_to, window_end)
|
||||
var part_amount: Variant = remaining_amount
|
||||
if (E.gt(duration, 0) and E.lt(part_end, created_to)):
|
||||
part_amount = E.subtract(E.floor_value(E.divide(E.multiply(amount, (E.subtract(part_end, created_from))), duration)), (E.subtract(amount, remaining_amount)))
|
||||
part_amount = E.subtract(E.floor_value(E.mul_div(amount, (E.subtract(part_end, created_from)), duration)), (E.subtract(amount, remaining_amount)))
|
||||
if E.le(E.add(window_end, lifespan), completed_before):
|
||||
# Retain the original integer allocation and lifetime integral, but
|
||||
# do not allocate thousands of already-dead batch dictionaries.
|
||||
@@ -132,7 +132,7 @@ func withdraw_batches(target: Dictionary, id: String, to_year: Variant) -> Array
|
||||
var until: Variant = E.minimum(to_year, end)
|
||||
if E.lt(to_year, end):
|
||||
var carry: Variant = batch.get("withdraw_remainder", 0)
|
||||
var expected: Variant = E.add(carry, E.divide(E.multiply((E.subtract(amount, carry)), (E.subtract(to_year, start))), (E.subtract(end, start))))
|
||||
var expected: Variant = E.add(carry, E.mul_div((E.subtract(amount, carry)), (E.subtract(to_year, start)), (E.subtract(end, start))))
|
||||
consumed = E.floor_value(expected)
|
||||
batch.withdraw_remainder = E.subtract(expected, consumed)
|
||||
batch.amount = E.maximum(0, E.subtract(amount, consumed))
|
||||
@@ -160,7 +160,7 @@ func expired_years(year: Variant, start: Variant, end: Variant) -> Variant:
|
||||
return E.subtract(year, start)
|
||||
if E.ge(year, end):
|
||||
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5")))
|
||||
return E.divide(E.multiply((E.subtract(year, start)), (E.subtract(year, start))), (E.multiply(2, (E.subtract(end, start)))))
|
||||
return E.mul_div((E.subtract(year, start)), (E.subtract(year, start)), (E.multiply(2, (E.subtract(end, start)))))
|
||||
|
||||
|
||||
func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
|
||||
|
||||
@@ -52,7 +52,7 @@ var speed: int :
|
||||
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
|
||||
|
||||
func evolute(delta: Variant) -> void:
|
||||
if (E.le(delta, 0) or (not E.finite(delta))):
|
||||
if not E.finite(delta) or E.approx(delta) <= 0.0 or E.approx(delta) * E.approx(state.speed_in_year) >= 1e27:
|
||||
return
|
||||
var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed)
|
||||
if not E.finite(clock.datetime): return
|
||||
|
||||
@@ -20,7 +20,7 @@ static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||
var order: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in requests:
|
||||
var expected: Variant = (E.divide(E.multiply(E.floor_value(E.maximum(0, requests[id])), available), total) if E.gt(total, 0) else 0)
|
||||
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])})
|
||||
@@ -38,7 +38,8 @@ static func production_step(costs: Array):
|
||||
var step = 1
|
||||
for cost in costs:
|
||||
if E.le(cost, 0): continue
|
||||
var denominator = E.make(E.parts(cost)[1])
|
||||
var terms: Array = E.parts(cost)
|
||||
var denominator = E.make(E.Big.divmod(terms[1], E.Big.gcd(terms[0], terms[1]))[0])
|
||||
step = E.multiply(E.divide(step, whole_gcd(step, denominator)), denominator)
|
||||
return step
|
||||
|
||||
@@ -76,12 +77,12 @@ static func split(remainders: Dictionary, key: String, count: Variant, weights:
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: Variant = 0
|
||||
for id in weights:
|
||||
expected[id] = E.add(E.divide(E.multiply(count, weights[id]), sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||
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]))
|
||||
var order: Array = []
|
||||
var assigned: Variant = 0
|
||||
for id in weights:
|
||||
var normalized: Variant = (E.divide(E.multiply(E.maximum(0, expected[id]), count), positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||
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])})
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const STATE_VERSION: String = "0.2.0"
|
||||
const STATE_VERSION: String = "0.3.0"
|
||||
const TICKS_PER_YEAR: int = 1000000000
|
||||
|
||||
static func processed_year(datetime):
|
||||
return E.divide(E.floor_value(E.multiply(datetime, 10)), 10)
|
||||
var whole = E.floor_value(datetime)
|
||||
var ticks: int = roundi(E.approx(E.subtract(datetime, whole)) * TICKS_PER_YEAR)
|
||||
return E.add(whole, E.divide(ticks / 100000000, 10))
|
||||
|
||||
static func elapsed_years(from_year, to_year):
|
||||
return E.subtract(to_year, from_year)
|
||||
|
||||
## Frame fractions stay small; the universe age is an exact integer tick count.
|
||||
## Only the fractional year is converted to ticks; the whole age stays exact.
|
||||
static func advance(time: Dictionary, delta: float, speed: int) -> Dictionary:
|
||||
var ticks = E.round_value(E.multiply(time.datetime, TICKS_PER_YEAR))
|
||||
var increment: float = delta * pow(10.0, speed - 1) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0))
|
||||
var whole_years = E.floor_value(time.datetime)
|
||||
var ticks: int = roundi(E.approx(E.subtract(time.datetime, whole_years)) * TICKS_PER_YEAR)
|
||||
var elapsed_years_float: float = delta * pow(10.0, speed - 1)
|
||||
var elapsed_whole = E.of(floorf(elapsed_years_float))
|
||||
var increment: float = (elapsed_years_float - floorf(elapsed_years_float)) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0))
|
||||
var whole: float = roundf(increment) if absf(increment - roundf(increment)) < 1e-6 else floorf(increment)
|
||||
ticks = E.add(ticks, E.of(whole))
|
||||
var datetime = E.divide(ticks, TICKS_PER_YEAR)
|
||||
return {"datetime": datetime, "year": processed_year(datetime), "ticks": ticks, "tick_remainder": increment - whole}
|
||||
var elapsed = E.add(elapsed_whole, E.divide(E.of(whole), TICKS_PER_YEAR))
|
||||
var datetime = E.add(whole_years, E.add(E.divide(ticks, TICKS_PER_YEAR), elapsed))
|
||||
return {"datetime": datetime, "year": processed_year(datetime), "tick_remainder": increment - whole}
|
||||
|
||||
@@ -102,7 +102,7 @@ func apply_blast(target: Dictionary, power: Variant, environment: Dictionary) ->
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(weights)
|
||||
# Fractional exposure is deterministic; destruction itself remains whole.
|
||||
for id in distribution:
|
||||
destroy_planets(target, id, E.multiply(E.divide(E.multiply(hits, distribution[id]), quantities[id]), environment.density), 1, "blast")
|
||||
destroy_planets(target, id, E.multiply(E.mul_div(hits, distribution[id], quantities[id]), environment.density), 1, "blast")
|
||||
|
||||
|
||||
func uses_short_lived_fast_path(id: String) -> bool:
|
||||
|
||||
@@ -6,20 +6,22 @@ func stellar_environment(source: Dictionary) -> Dictionary:
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(source)
|
||||
# SMBH is a self-group in GI; its quantity stores mass, not object count.
|
||||
var mass: Variant = source.amount.get("SMBH", 0)
|
||||
var volume: Variant = quantities.get("S", 0)
|
||||
# Geometric volume includes a 1e22 scale per void and can exceed E27.
|
||||
# It only feeds approximate density/blast coefficients, never stock counts.
|
||||
var volume: float = E.approx(quantities.get("S", 0))
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if E.contains(["H", "He", "Met", "SMBH"], id):
|
||||
mass = E.add(mass, quantities[id])
|
||||
else:
|
||||
mass = E.add(mass, E.multiply(quantities[id], E.maximum(0, entity.mass)))
|
||||
volume = E.add(volume, E.multiply(quantities[id], E.maximum(0, entity.space)))
|
||||
volume = E.add(volume, E.multiply(E.multiply(quantities[id], E.maximum(0, entity.voids)), E.of("1.0e22")))
|
||||
volume += E.approx(quantities[id]) * maxf(0.0, E.approx(entity.space))
|
||||
volume += E.approx(quantities[id]) * maxf(0.0, E.approx(entity.voids)) * 1e22
|
||||
var metallicity: Variant = 0
|
||||
if E.gt(mass, 0):
|
||||
metallicity = E.maximum(0, E.add(E.multiply(E.of("0.4"), E.logarithm(E.add(E.divide(E.multiply(100, source.amount.get("Met", 0)), mass), E.of("0.11")))), E.of("0.88")))
|
||||
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)))))
|
||||
return {"metallicity": metallicity, "density": E.divide(E.multiply(E.of("1.23456e-7"), E.square_root(E.add(base_mass, mass))), E.power(E.maximum(1, volume), E.divide(1, 6))), "blast": (E.clamp_value(E.divide(source.amount.get("S", 0), volume), 0, 1) if E.gt(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}
|
||||
|
||||
|
||||
func modify_world_parameter(key: String, change: Variant) -> void:
|
||||
|
||||
@@ -91,7 +91,7 @@ static func _integration(target: Dictionary, events: Variant) -> void:
|
||||
var chosen: int = rng.randi_range(0, 2)
|
||||
counts[chosen] = E.add(counts[chosen], 1)
|
||||
else:
|
||||
counts[0] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(events, 3)), E.approx(E.square_root(E.divide(E.multiply(events, 2), 9)))))), 0, events)
|
||||
counts[0] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(events, 3)), E.approx(E.square_root(E.mul_div(events, 2, 9)))))), 0, events)
|
||||
var rest: Variant = E.subtract(events, counts[0])
|
||||
counts[1] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(rest, 2)), E.approx(E.square_root(E.divide(rest, 4)))))), 0, rest)
|
||||
counts[2] = E.subtract(rest, counts[1])
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
[ext_resource type="Texture2D" uid="uid://so06vth6le3b" path="res://ui/main_menu/ru/russian_active.png" id="51_uvuuk"]
|
||||
[ext_resource type="Texture2D" uid="uid://dwqpcm1pryw44" path="res://ui/main_menu/en/leave_reality.png" id="52_uvuuk"]
|
||||
[ext_resource type="Texture2D" uid="uid://btghvfdh3guo5" path="res://ui/report/en/report.png" id="60_an6vj"]
|
||||
[ext_resource type="Texture2D" uid="uid://dvt8fw1m5mpki" path="res://ui/main_menu/sound/sound_runner.png" id="61_bemti"]
|
||||
[ext_resource type="Texture2D" uid="uid://bce2bpw5642er" path="res://ui/achievements/achievements.en.png" id="61_d36xh"]
|
||||
[ext_resource type="Script" uid="uid://vy0dj2cqfmk0" path="res://ui/credits/credits.gd" id="62_o2i0n"]
|
||||
[ext_resource type="Texture2D" uid="uid://c46h78ewwlafk" path="res://ui/credits/credits.en.png" id="62_vu83j"]
|
||||
@@ -986,11 +987,15 @@ theme_override_constants/separation = 64
|
||||
[node name="SoundVolume" type="HSlider" parent="Stage/MainMenu/Volume" unique_id=1297986446]
|
||||
custom_minimum_size = Vector2(444, 96)
|
||||
layout_mode = 2
|
||||
theme_override_icons/grabber = ExtResource("61_bemti")
|
||||
theme_override_icons/grabber_highlight = ExtResource("61_bemti")
|
||||
scrollable = false
|
||||
|
||||
[node name="MusicVolume" type="HSlider" parent="Stage/MainMenu/Volume" unique_id=729455266]
|
||||
custom_minimum_size = Vector2(444, 96)
|
||||
layout_mode = 2
|
||||
theme_override_icons/grabber = ExtResource("61_bemti")
|
||||
theme_override_icons/grabber_highlight = ExtResource("61_bemti")
|
||||
scrollable = false
|
||||
|
||||
[node name="Credits" type="TextureButton" parent="Stage/MainMenu" unique_id=44726060]
|
||||
|
||||
Reference in New Issue
Block a user