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:
Kirill
2026-10-01 07:48:52 +03:00
parent fad862d618
commit befb377774
58 changed files with 378 additions and 262 deletions
+3 -6
View File
@@ -6,7 +6,7 @@ const E = preload("res://world/owniverse/math/exact.gd")
const SETTING_SECTION = "Settings" const SETTING_SECTION = "Settings"
const SAVE_PATH = "user://settings.cfg" 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 SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd") const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
const STATE_VERSION : String = SimulationTime.STATE_VERSION 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: 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: 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" return "unsupported state.version"
if (((not (saved.time is Dictionary)) or (not _number(saved.time.get("datetime")))) or (not _number(saved.time.get("year")))): if (((not (saved.time is Dictionary)) or (not _number(saved.time.get("datetime")))) or (not _number(saved.time.get("year")))):
return "invalid state.time" 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" 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 saved.time.has("tick_remainder"):
if not E.finite(saved.time.tick_remainder) or E.ge(E.absolute(saved.time.tick_remainder), 1): if not E.finite(saved.time.tick_remainder) or E.ge(E.absolute(saved.time.tick_remainder), 1):
return "invalid fractional tick" return "invalid fractional tick"
+15
View File
@@ -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
View File
@@ -1,9 +1,10 @@
# Эволюция звёзд Owniverse.GI # Эволюция звёзд Owniverse.GI
Числа симуляции переведены на целочисленное хранение с большими разрядами и Числа симуляции хранят целую часть в двух блоках int64, дробный остаток — в
рациональную арифметику. Количества до `10^27` сохраняют единицы; время хранится float. Весь порядок E27 сохраняет целые единицы. Возраст разделён на целые
в тиках по `10^-9` года. Границы точности коэффициентов, ограничение накопленных годы и дробную часть с шагом 10^-9 года; большие промежуточные произведения
дробей и несовместимый со старым формат сохранения 3 описаны в распределений вычисляются через `mul_div`. Границы точности, допуск переноса
дробей и несовместимый со старым формат сохранения 4 описаны в
[документации математики](world/owniverse/math/README.md). [документации математики](world/owniverse/math/README.md).
Механики перенесены из Owniverse.U с согласованными изменениями GI: постепенные партии, детерминированные дробные остатки, уязвимость цивилизаций до C1.2 и передача выживших планет преемнику. Баланс таблицы GI сохраняется. Механики перенесены из 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
View File
@@ -6,24 +6,25 @@
godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json
``` ```
Читает диагностический JSON формата 3 в отдельные экземпляры мира и замеряет Читает диагностический JSON формата 4 в отдельные экземпляры мира и замеряет
шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается. шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается.
Последним аргументом можно задать длительность шага в реальных секундах Последним аргументом можно задать длительность шага в реальных секундах
(по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса. (по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса.
При проверке регрессии на копии сохранения с 120 партиями шаг на скорости 6 На диагностической копии с 120 партиями прежняя рациональная арифметика после
занимал около 1,3 секунды. Главная причина — перебор пустых завершённых окон оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6.
коротких взрывов; дополнительная — повторное сокращение уже сокращённых дробей. После перехода на целые блоки и float-остаток повторный замер дал 58–62 мс.
После пропуска таких окон и устранения повторных НОД замеры составили 90–99 мс При шаге 0,1 секунды: 71–82 мс вместо 117–138 мс. Это замеры расчёта на данной
на шаг 1/60 секунды. Это ускорение примерно в 14 раз, но ещё не бюджет 60 FPS. копии, а не обещание FPS. Для сравнения старый JSON был преобразован только
Размер партии, точность чисел и правила эволюции не изменены. Разреженные во временный тестовый файл; в загрузчик игры миграция не добавлена.
сценарии дополнительно сравниваются с полным расчётом в `test_short_lived_events.gd`. Разреженные сценарии сравниваются с полным расчётом в `test_short_lived_events.gd`.
## Точная арифметика ## Целые блоки и дробные остатки
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.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 циклов с меняющимися рецепты с дробными стоимостями, сохранение сумм, время и 100 циклов с меняющимися
долями. Все остальные тесты используют числовое сравнение и сериализацию через долями. Все остальные тесты используют числовое сравнение и сериализацию через
`E`, а не сравнение ссылок на большие числа. Границы точности и новый формат `E`, а не сравнение ссылок на большие числа. Границы точности и новый формат
@@ -90,14 +91,14 @@ godot --headless --path Owniverse.GI --script res://tests/test_time.gd
Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы. Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.
Схема состояния `0.1.1` хранит: Схема состояния `0.3.0` хранит:
- `time.datetime` — накопленный возраст, включая необработанную дробную часть; - `time.datetime` — накопленный возраст, включая необработанную дробную часть;
- `time.year` — последний рассчитанный возраст с точностью до десятой доли года. - `time.year` — последний рассчитанный возраст с точностью до десятой доли года.
Сохранения используют формат 2 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют. Сохранения используют формат 4 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 2. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись. Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 4. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
## Согласованные правила эволюции ## Согласованные правила эволюции
+2 -2
View File
@@ -22,8 +22,8 @@ func run():
quit(1) quit(1)
return return
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0])) var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
if not data is Dictionary or data.get("format_version") != 3: if not data is Dictionary or data.get("format_version") != 4:
push_error("Expected diagnostic JSON format 3") push_error("Expected diagnostic JSON format 4")
quit(1) quit(1)
return return
var initial: Dictionary = E.decode(restore_json_integers(data)).state var initial: Dictionary = E.decode(restore_json_integers(data)).state
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -16,7 +16,7 @@ func check(value: bool, message: String) -> void:
func run() -> void: func run() -> void:
var actions = load("res://ui/actions/actions.tscn").instantiate() var actions = load("res://ui/actions/actions.tscn").instantiate()
root.add_child(actions) 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) actions.set_actions("test", maximum)
var labels: Array = [] var labels: Array = []
var values: Array = [] var values: Array = []
+1 -1
View File
@@ -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)) 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(): 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") 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: func run() -> void:
var global: Node = root.get_node("Global") var global: Node = root.get_node("Global")
+2 -2
View File
@@ -59,7 +59,7 @@ func run() -> void:
world.state.amount["C0.2"] = 3 world.state.amount["C0.2"] = 3
world.evolution.initialize_life_batches() world.evolution.initialize_life_batches()
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population") 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() var restored = fixture()
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state)))) restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
restored.evolution.initialize_life_batches() restored.evolution.initialize_life_batches()
@@ -117,7 +117,7 @@ func run() -> void:
world.evolution.initialize_life_batches() world.evolution.initialize_life_batches()
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent") 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(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) dispose(world)
world = fixture() world = fixture()
+30 -10
View File
@@ -15,20 +15,27 @@ func check(actual, expected: String, label: String):
failures += 1 failures += 1
push_error(label + ": " + str(actual) + " != " + expected) 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(): func run():
# Expected strings were generated independently with Python fractions.Fraction. # 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")) var cases = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/exact_oracle.json"))
for row in cases: for row in cases:
var a = E.of(row[0]) var a = E.of(row[0])
var b = E.of(row[1]) var b = E.of(row[1])
check(E.add(a, b), row[2], "sum") check_number(E.add(a, b), row[2], "sum")
check(E.subtract(a, b), row[3], "difference") check_number(E.subtract(a, b), row[3], "difference")
check(E.multiply(a, b), row[4], "product") check_number(E.multiply(a, b), row[4], "product")
check(E.divide(a, b), row[5], "quotient") check_number(E.divide(a, b), row[5], "quotient")
check(E.floor_value(E.divide(a, b)), row[6], "floor") check(E.floor_value(E.divide(a, b)), row[6], "floor")
for text in ["9223372036854775807", "-9223372036854775808", "9007199254740993", "1000000000000000000000000001", "-1/10000000000000"]: for text in ["9223372036854775807", "-9223372036854775808", "9007199254740993", "1000000000000000000000000001", "-1/10000000000000"]:
var value = E.of(text) 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.add(E.of("9223372036854775807"), 1), "9223372036854775808", "int64 overflow promoted")
check(E.subtract(E.of("-9223372036854775808"), 1), "-9223372036854775809", "int64 underflow promoted") check(E.subtract(E.of("-9223372036854775808"), 1), "-9223372036854775809", "int64 underflow promoted")
check(Q.production_step([E.of("1e27")]), "1", "large integral recipe") 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(Q.sum(allocation), str(count), "allocation conserves every unit")
check(allocation.a, "333333333333333333333333334", "deterministic remainder") check(allocation.a, "333333333333333333333333334", "deterministic remainder")
var clock = Clock.advance({"datetime": E.of("1e27")}, 0.000000001, 1) 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() var world = load("res://world/owniverse/owniverse.gd").new()
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities) world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
for id in world.catalog.entities: for id in world.catalog.entities:
@@ -52,10 +59,23 @@ func run():
check(world.state.energy, "999999999999999999999999999", "spend one energy") check(world.state.energy, "999999999999999999999999999", "spend one energy")
var life = world.life.life_products(world.state, world.catalog.entities.EON5, count) var life = world.life.life_products(world.state, world.catalog.entities.EON5, count)
check(Q.sum(life), str(count), "life conservation at 1e27") 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.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.S = E.of("1e24")
world.state.amount.H = E.of("1e24") world.state.amount.H = E.of("1e24")
world.state.amount.RD = 1234567 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") 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)) print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
var saves = root.get_node("SaveManager") 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() for entity in world.catalog.entities.values(): entity.free()
world.free() world.free()
print("Exact math: %d checks, %d failures" % [checks, failures]) print("Exact math: %d checks, %d failures" % [checks, failures])
+2 -2
View File
@@ -54,7 +54,7 @@ func run() -> void:
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt") check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
var physical: Dictionary = world.ledger.physical_amounts(world.state) 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(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() var restored = fixture()
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state)))) restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
world.evolution.evolve_batches(world.state, 11000000) world.evolution.evolve_batches(world.state, 11000000)
@@ -118,7 +118,7 @@ func run() -> void:
world.evolution.evolve_batches(world.state, 2050) 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.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(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) dispose(world)
print(("Life: %d checks, %d failures" % [checks, failures])) print(("Life: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1)) quit((0 if E.eq(failures, 0) else 1))
+13
View File
@@ -43,6 +43,19 @@ func run() -> void:
world.catalog.entities.N.bind = {"S": 3} world.catalog.entities.N.bind = {"S": 3}
world.state.amount.H = 10 world.state.amount.H = 10
world.state.amount.S = 0 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.auto_resources = {"H": 10}
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}] world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
var before: Dictionary = world.state.duplicate(true) var before: Dictionary = world.state.duplicate(true)
+3 -3
View File
@@ -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.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") 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") 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 = fixture()
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state)))) 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")) 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") 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)) 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(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) dispose(world)
world = fixture() 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.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(((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((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) dispose(world)
world = fixture() world = fixture()
+3 -3
View File
@@ -48,13 +48,13 @@ func run() -> void:
world.evolute(E.of("0.2")) 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") 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) 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) world.evolute(invalid)
check(E.eq(world.state, before), "Invalid/overflow interval rejected") 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) var invalid_save: Dictionary = world.state.duplicate(true)
invalid_save.time.year = E.of("0.25") 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) dispose(world)
for fps in [30, 60, 144]: for fps in [30, 60, 144]:
+1 -1
View File
@@ -133,7 +133,7 @@ func run() -> void:
Parameters.advance(source, world.state, rules, E.of("1.0e12")) 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.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") 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") var manager: Node = root.get_node("SaveManager")
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation") check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
var old: Dictionary = payload.duplicate(true) var old: Dictionary = payload.duplicate(true)
+1
View File
@@ -43,6 +43,7 @@ func _get_distribution(value: Variant) -> Array:
while E.lt(power, value): while E.lt(power, value):
distribution.append(power) distribution.append(power)
if E.gt(distribution.size(), 4): distribution.pop_front() if E.gt(distribution.size(), 4): distribution.pop_front()
if E.ge(power, E.of("1e27")): break
power = E.multiply(power, 10) power = E.multiply(power, 10)
distribution.append(value) distribution.append(value)
while E.lt(distribution.size(), 5): distribution.append(0) while E.lt(distribution.size(), 5): distribution.append(0)
+1 -1
View File
@@ -5,7 +5,7 @@
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии. - `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
- `entity_catalog.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: освобождает их владелец мира, как до рефакторинга. Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
+3 -2
View File
@@ -18,9 +18,10 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
var automatic: Dictionary = {} var automatic: Dictionary = {}
for id in quantities: for id in quantities:
var entity: OwniverseEntity = world.catalog.entities[id] 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 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 auto_share: Variant = E.clamp_value(entity.auto, 0, 1)
var products: Dictionary = yearly_products(entity)
for product in products: for product in products:
var amount: Variant = E.multiply(E.multiply(object_years, products[product]), WorldParameters.production(previous, entity)) 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)))) 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) var cost: Variant = entity.cost.get("H", 0)
if E.le(cost, 0): if E.le(cost, 0):
return 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 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")) 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)) quota = E.minimum(quota, E.divide((E.add(available, fuel)), cost))
+1 -1
View File
@@ -231,4 +231,4 @@ static func effective_span(world, target: Dictionary, id: String, rules: Diction
var weighted: Variant = population var weighted: Variant = population
for home in target.civilization_homes.get(id, {}): 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)))) 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)
+1 -1
View File
@@ -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 h: Variant = world.state.amount.get("H", 0)
var mixture: String = "—" var mixture: String = "—"
if E.gt(E.add(he, h), 0): 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) 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": "—"} 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": "—"}
+2 -2
View File
@@ -13,7 +13,7 @@ func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant)
target.life_remainders = {} target.life_remainders = {}
for channel in ["progress", "degradation"]: for channel in ["progress", "degradation"]:
var key: String = E.add(E.add(entity.id, ":"), channel) 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 target.life_remainders[key] = expected
pending[channel] = E.floor_value(expected) pending[channel] = E.floor_value(expected)
var assigned: Dictionary = QuantityMath.allocate_whole(pending, count) var assigned: Dictionary = QuantityMath.allocate_whole(pending, count)
@@ -31,7 +31,7 @@ func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant)
var requests: Dictionary = {} var requests: Dictionary = {}
for id in weights: for id in weights:
var key: String = E.add(E.add(entity.id, ":fall:"), id) 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 target.life_remainders[key] = expected
requests[id] = E.maximum(0, E.floor_value(expected)) requests[id] = E.maximum(0, E.floor_value(expected))
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation) var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
+4 -4
View File
@@ -74,7 +74,7 @@ func plan_smbh(desired_amount: Variant) -> Dictionary:
var order: Array = [] var order: Array = []
var consumed_mass: Variant = 0 var consumed_mass: Variant = 0
for id in distribution: 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])) 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)) 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])}) 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())) 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"): 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 = {} var requirements: Dictionary = {}
for resource in entity.cost: for resource in entity.cost:
requirements[resource] = E.maximum(0, entity.cost[resource]) 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. # No recipe means unavailable for production, not unlimited free objects.
if (not has_requirements): if (not has_requirements):
return 0 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: for resource in requirements:
if E.gt(requirements[resource], 0): if E.gt(requirements[resource], 0):
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource]))) available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource])))
+73 -59
View File
@@ -1,82 +1,96 @@
# Числа симуляции # Числа симуляции
Количество объектов, запасы, энергия, расходы, привязки и статистика считаются `exact.gd` сохраняет единый API `E` для симуляции и интерфейса. Целые числа,
целочисленно, без перехода через `float`. Значения до `10^27` сохраняют каждую помещающиеся в `int64`, остаются обычными `int`. Остальные значения хранятся
единицу; промежуточные произведения могут превышать этот диапазон. в `exact_value.gd`:
`big_integer.gd` хранит модуль числа массивом разрядов по основанию `10^9`, ```
младший разряд первым. Переносы при умножении не переполняют `int64`. polarity × (high × 10^14 + low + fraction)
Деление использует нормализованную оценку цифры частного. ```
`exact.gd` — единый API арифметики. Малые значения остаются обычными `int`, `high` и `low` — целые блоки от 0 до 10^14−1, `fraction` — `float` от 0 до 1,
большие числа и дроби представлены `exact_value.gd`: знак, целый числитель, `polarity` — знак. Значения неизменяемы после создания. Диапазон включает весь
целый знаменатель. Дроби сокращаются. Операции создают новые значения; порядок E27: модуль целой части меньше 10^28. Каждая единица целой части
поля `ExactValue` и массивы его разрядов нельзя изменять после создания. сохраняется, в том числе при добавлении и вычитании единицы около 10^27.
Выход за диапазон проверяется утверждениями; насыщения или обрезания запасов нет.
Числители и знаменатели в состоянии больше не хранятся. НОД при обычных
арифметических операциях не вычисляется; обход состояния для ограничения дробей
больше не нужен. `bounded` и `bound_state` оставлены как совместимые пустые операции.
## Операции
Сложение, вычитание и сравнение работают непосредственно с блоками.
Для умножения и деления `big_integer.gd` используется как временный вычислитель
с безопасными разрядами по основанию 10^9. Результат сразу возвращается в
фиксированную структуру. Размер временных дробей определяется входными float,
а не растёт от шага к шагу.
`E.mul_div(a, b, denominator)` вычисляет отношение за одну операцию. Это нужно
для распределений: произведение двух количеств может превышать E27, хотя итог
деления укладывается в диапазон. Промежуточное произведение не становится
значением симуляции. Целое частное вычисляется до перевода остатка в float.
```gdscript ```gdscript
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
var stock = E.of("1e27") # Строка, не литерал float 1e27! var stock = E.of("1e27") # Большое целое вводится строкой.
stock = E.subtract(stock, 1) # 999999999999999999999999999 stock = E.subtract(stock, 1)
var price = E.of("0.125") # Точно 1/8 var share = E.mul_div(stock, 15, 100) # Точные 15% целого запаса.
var cost = E.multiply(price, 8) # Точно 1 var whole_share = E.floor_value(share)
if E.ge(stock, cost):
stock = E.subtract(stock, cost)
``` ```
Использовать `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge`, Использовать `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge`,
`E.floor_value/ceil_value/round_value`. Перегрузки операторов в GDScript нет: `E.floor_value/ceil_value/round_value`. Обычные операторы над объектами
обычные `+`, `==`, `min`, `float(...)` для чисел симуляции не подходят. `ExactValue` не подходят. `E.integer` допускается только для значений int64;
Сигнатуры принимают `Variant`, а не `float`. `E.integer` допускается только `E.approx` — явный переход к приближённому float.
для значений в диапазоне `int64`, например индексов и состояния генератора.
Для поиска чисел в массиве используется `E.contains`, поскольку сравнение
объектов по ссылке не является сравнением числовых значений.
## Границы точности ## Дробная точность
- Десятичные параметры CSV разбираются из строк точно. Стоимости, проверка Дробная часть приближённая. Для временного умножения/деления она преобразуется
доступности и размер целой производственной партии не округляются. в десятичную дробь с 16 значащими цифрами; нормализация сохраняет значащие цифры
- Суммы распределений и переносы целых объектов сохраняются точно. малых коэффициентов. Десятичные стоимости вроде 0.125 и 0.0000000000001
- Промежуточная рациональная арифметика точна. Перед публикацией следующего сохраняют корректный размер производственной партии. НОД применяется только
состояния знаменатели сохранённых дробей ограничиваются `10^36`. при определении знаменателя стоимости в `production_step`.
Если знаменатель больше, значение усекается к нулю с ошибкой меньше
`10^-36`. Целые числа эта операция не меняет. Это предотвращает рост
знаменателей при многократном накоплении меняющихся долей. Такая точность
относится к каждой операции ограничения, а не является обещанием нулевой
суммарной ошибки при бесконечном числе циклов.
- Логарифмы, корни, нецелые степени и нормальная случайная выборка остаются
приближёнными. Результаты трансцендентных функций переводятся обратно в
рациональные коэффициенты с 12 знаками после точки. Статистическая выборка
не обеспечивает равномерный выбор каждого отдельного целого около `10^27`;
последующий учёт выбранного количества и остатка точен.
- Возраст хранится в целых тиках (`10^9` тиков на год). Малый остаток кадрового
времени остаётся `float` и переносится между кадрами; большой возраст
никогда не складывается с малым приращением через `float`.
- Интерфейс преобразует в `float` только координаты индикаторов. Текст чисел,
доступное количество и значения кнопок формируются из точных чисел.
`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**. На главном потоке Формат сохранения — **4**, версия состояния — **0.3.0**. Большие целые
`E.encode` строит независимый снимок, затем существующий фоновый писатель кодируются как `{"$exact": "целое число"}`. Блоки с дробью кодируются как
сохраняет его. Большие целые и дроби кодируются как `{"$exact": "число"}`; `{"$fixed": ["high", "low", "float bytes in hex", sign]}`. Восемь байтов float
для дробей используется строка `числитель/знаменатель`. Целые за пределами сохраняются без потери точности JSON, включая очень малые остатки.
точного диапазона JSON/float тоже кодируются строкой. Зашифрованный файл и Зашифрованный файл и диагностический JSON используют один снимок.
диагностический JSON используют один снимок. После проверки `E.decode`
восстанавливает числа, без загрузки сериализованных объектов GDScript.
Миграции старой арифметики нет. Старый формат несовместим: существующий Миграции нет. Сохранение прежнего формата при обычной загрузке архивируется
механизм загрузки сохраняет его в `.incompatible.bak` и начинает новую игру. существующим механизмом `.incompatible.bak`; начинается новая игра.
Старое сохранение игрока не используется тестами и не изменялось при разработке. Тесты не загружают и не перезаписывают рабочее сохранение игрока.
## Проверки ## Проверки
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd` `godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python
`fractions.Fraction` (120 пар, seed 4321). Тест сравнивает строковые результаты `fractions.Fraction` (160 пар, seed 4321). Произведения находятся внутри E27;
суммы, разности, произведения, деления и округления вниз. Дополнительно дробные результаты сравниваются с абсолютным допуском 10^-12, целые — точно.
проверяются границы `int64`, JSON, рецепты, распределения, жизнь при `10^27`, Дополнительно проверяются границы int64/E27, единичные изменения, точные
единичный расход энергии, тик времени и 100 циклов с меняющимися долями звёзд. проценты, объединённое умножение с делением, JSON, дробные рецепты,
распределения, жизнь, энергия, время, форматирование и 100 циклов симуляции.
+152 -114
View File
@@ -1,56 +1,76 @@
extends RefCounted 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 Big = preload("res://world/owniverse/math/big_integer.gd")
const Value = preload("res://world/owniverse/math/exact_value.gd") const Value = preload("res://world/owniverse/math/exact_value.gd")
const SMALL: int = 1000000000 const SMALL: int = 1000000000
const MAX_INT: int = 9223372036854775807 const MAX_INT: int = 9223372036854775807
const MIN_INT: int = -9223372036854775807 - 1 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): static func make(n: Array, d: Array = [1], polarity: int = 1):
assert(d != [0], "Zero denominator") assert(d != [0], "Zero denominator")
if n == [0]: return 0 var qr: Array = Big.divmod(n, d)
if d != [1]: var fraction: float = 0.0
var common: Array = Big.gcd(n, d) if qr[1] != [0]:
n = Big.divmod(n, common)[0] var numerator_text: String = Big.decimal(qr[1])
d = Big.divmod(d, common)[0] var denominator_text: String = Big.decimal(d)
return _make_reduced(n, d, polarity) 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 _from_whole(n: Array, fraction: float, polarity: int):
static func _make_reduced(n: Array, d: Array, polarity: int): var text: String = Big.decimal(n)
if n == [0]: return 0 assert(Big.compare(n, LIMIT) < 0, "Number exceeds E27 range")
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)): var high: int = int(text.left(text.length() - 14)) if text.length() > 14 else 0
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0)) var low: int = int(text.right(14))
if d == [1] and n.size() == 3: return _compose(high, low, fraction, polarity)
var limit: Array = [854775807 if polarity > 0 else 854775808, 223372036, 9]
if Big.compare(n, limit) <= 0: static func _compose(high: int, low: int, fraction: float, polarity: int):
return (("-" if polarity < 0 else "") + Big.decimal(n)).to_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() var result = Value.new()
result.numerator = n result.high = high
result.denominator = d result.low = low
result.fraction = fraction
result.polarity = polarity result.polarity = polarity
return result 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): static func of(value):
if value is Value or value is int: return value if value is Value or value is int: return value
if value is float: if value is float:
if not is_finite(value): return value if not is_finite(value): return value
if value == floorf(value) and absf(value) < 1e18: return int(value) if value == floorf(value) and absf(value) < 1e18: return int(value)
# Imports the actual binary float, not an invented decimal rounding. var magnitude: float = absf(value)
var data := PackedByteArray() return _from_whole(Big.parse("%.0f" % floorf(magnitude)), magnitude - floorf(magnitude), -1 if value < 0 else 1)
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 text: String = str(value).strip_edges().to_lower() var text: String = str(value).strip_edges().to_lower()
if text == "": return 0 if text == "": return 0
if "/" in text: if "/" in text:
@@ -72,7 +92,17 @@ static func of(value):
static func parts(value) -> Array: static func parts(value) -> Array:
value = of(value) value = of(value)
if value is int: return [Big.parse(str(value).trim_prefix("-")), [1], -1 if value < 0 else 1] 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: static func numeric(value) -> bool:
return value is int or value is float or value is Value 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: static func approx(value) -> float:
if not value is Value: return float(value) if not value is Value: return float(value)
var a: String = Big.decimal(value.numerator) return value.polarity * (float(value.high) * BLOCK + float(value.low) + value.fraction)
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))
static func compare(a, b) -> int: 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 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 x: float = approx(a)
var y: float = approx(b) var y: float = approx(b)
return 0 if x == y else (1 if x > y else -1) return 0 if x == y else (1 if x > y else -1)
var x: Array = parts(a) var x: Array = _blocks(a)
var y: Array = parts(b) var y: Array = _blocks(b)
if x[2] != y[2]: return 1 if x[2] > y[2] else -1 if x[3] != y[3]: return 1 if x[3] > y[3] else -1
return x[2] * Big.compare(Big.multiply(x[0], y[1]), Big.multiply(y[0], x[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: static func eq(a, b) -> bool:
if a == null: return b == null if a == null: return b == null
@@ -134,69 +164,68 @@ static func add(a, b):
if a is int and b is int: 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 (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 not finite(a) or not finite(b): return approx(a) + approx(b)
if a is int and a == 0: return of(b) var x: Array = _blocks(a)
if b is int and b == 0: return of(a) var y: Array = _blocks(b)
var x: Array = parts(a) if x[3] == y[3]:
var y: Array = parts(b) var fraction: float = x[2] + y[2]
var common: Array = Big.gcd(x[1], y[1]) # Repeated fractional accumulation may land just below an integer.
var xd: Array = Big.divmod(x[1], common)[0] # This tolerance only applies to the fractional carry, never whole blocks.
var yd: Array = Big.divmod(y[1], common)[0] if x[2] > 0.0 and y[2] > 0.0 and fraction > 0.5 and absf(fraction - roundf(fraction)) <= 1e-12:
var left: Array = Big.multiply(x[0], yd) fraction = roundf(fraction)
var right: Array = Big.multiply(y[0], xd) return _compose(x[0] + y[0], x[1] + y[1], fraction, x[3])
var denominator: Array = Big.multiply(xd, y[1]) var order: int = 0
var numerator: Array for i in 3:
var polarity: int = x[2] if x[i] != y[i]:
if x[2] == y[2]: order = 1 if x[i] > y[i] else -1
numerator = Big.add(left, right) break
else: if order == 0: return 0
var order: int = Big.compare(left, right) if order < 0:
numerator = Big.subtract(left, right) if order >= 0 else Big.subtract(right, left) var tmp: Array = x
polarity = x[2] if order >= 0 else y[2] x = y
# Reduced inputs can only share a remaining factor from gcd(d1, d2). y = tmp
# Testing the entire new denominator repeats expensive long division. return _compose(x[0] - y[0], x[1] - y[1], x[2] - y[2], x[3])
var reduction: Array = Big.gcd(numerator, common)
return _make_reduced(Big.divmod(numerator, reduction)[0], Big.divmod(denominator, reduction)[0], polarity)
static func subtract(a, b): static func subtract(a, b):
if not numeric(a) or not numeric(b): return a - b if not numeric(a) or not numeric(b): return a - b
if a is int and b is int: 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 (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 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 = _blocks(b)
var y: Array = parts(b) return add(a, _compose(y[0], y[1], y[2], -y[3]))
return add(a, _make_reduced(y[0], y[1], -y[2]))
static func multiply(a, b): static func multiply(a, b):
if not numeric(a) or not numeric(b): return 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 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 eq(a, 0) or eq(b, 0): return 0
if a is int and a == 1: return of(b) if eq(a, 1): return of(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 a != MIN_INT and b != MIN_INT: 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 if absi(a) <= MAX_INT / absi(b): return a * b
var x: Array = parts(a) var x: Array = parts(a)
var y: Array = parts(b) var y: Array = parts(b)
var g1: Array = Big.gcd(x[0], y[1]) return make(Big.multiply(x[0], y[0]), Big.multiply(x[1], y[1]), x[2] * y[2])
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]) ## 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): static func divide(a, b):
if not numeric(a) or not numeric(b): return 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 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 is int and b is int and b != 0 and not (a == MIN_INT and b == -1):
if a % b == 0: return a / b if a % b == 0: return a / b
var y: Array = parts(b) return mul_div(a, 1, b)
assert(y[0] != [0], "Exact division by zero")
return multiply(a, _make_reduced(y[1], y[0], y[2]))
static func floor_value(a): static func floor_value(a):
if not finite(a): return a if not finite(a) or a is int: return a
if a is int: return a var x: Array = _blocks(a)
var x: Array = parts(a) return _compose(x[0], x[1] + (1 if x[3] < 0 and x[2] > 0 else 0), 0.0, x[3])
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])
static func ceil_value(a): return neg(floor_value(neg(a))) 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")))) 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") assert(result is int, "Value does not fit native int64")
return result return result
## Bound persistent fractions, never integer quantities or recipe arithmetic. ## Kept as no-op API: float remainder storage is already bounded.
## Truncation towards zero changes a value by less than 10^-36. static func bounded(value): return value
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)
## Only call on a newly owned state snapshot, never on a published old state. static func bound_state(value): return value
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 power(a, b): 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): if finite(b) and eq(b, floor_value(b)) and le(absolute(b), 10000):
var exponent: int = integer(absolute(b)) var exponent: int = integer(absolute(b))
var result = 1 var result = 1
@@ -232,7 +250,7 @@ static func power(a, b):
if exponent % 2: result = multiply(result, a) if exponent % 2: result = multiply(result, a)
exponent /= 2 exponent /= 2
if exponent: a = multiply(a, a) 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))) return coefficient(pow(approx(a), approx(b)))
static func coefficient(value: float): static func coefficient(value: float):
@@ -253,21 +271,27 @@ static func contains(container, value) -> bool:
return value in container return value in container
static func fixed(value, digits: int = 0) -> String: static func fixed(value, digits: int = 0) -> String:
var scale = power(10, digits) if not finite(value): return str(value)
var rounded = round_value(multiply(value, scale)) var x: Array = parts(value)
var text: String = str(absolute(rounded)) 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: if digits:
text = text.lpad(digits + 1, "0") text = text.lpad(digits + 1, "0")
text = text.left(text.length() - digits) + "." + text.right(digits) 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: static func scientific(value, digits: int = 4) -> String:
if eq(value, 0): return fixed(0, digits) + "E+0" if eq(value, 0): return fixed(0, digits) + "E+0"
var terms: Array = parts(absolute(value)) var terms: Array = parts(absolute(value))
var exponent: int = Big.decimal(terms[0]).length() - Big.decimal(terms[1]).length() var exponent: int = Big.decimal(terms[0]).length() - Big.decimal(terms[1]).length()
if lt(absolute(value), power(10, exponent)): exponent -= 1 var scale = of("1e%d" % exponent)
var mantissa = divide(value, power(10, exponent)) if lt(absolute(value), scale):
if ge(absolute(round_value(multiply(mantissa, power(10, digits)))), multiply(10, power(10, digits))): exponent -= 1
scale = of("1e%d" % exponent)
var mantissa = divide(value, scale)
if float(fixed(absolute(mantissa), digits)) >= 10.0:
mantissa = divide(mantissa, 10) mantissa = divide(mantissa, 10)
exponent += 1 exponent += 1
return fixed(mantissa, digits) + ("E+%d" if exponent >= 0 else "E%d") % exponent 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 "") return ("-" if lt(value, 0) else "") + integer_text + result + ("." + parts_text[1] if parts_text.size() > 1 else "")
static func encode(value): 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): if value is int and (value > 9007199254740991 or value < -9007199254740991):
return {"$exact": str(value)} return {"$exact": str(value)}
if value is Dictionary: if value is Dictionary:
@@ -300,6 +328,9 @@ static func encode(value):
static func decode(value): static func decode(value):
if value is Dictionary: 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"): return of(value["$exact"])
var result: Dictionary = {} var result: Dictionary = {}
for k in value: result[k] = decode(value[k]) for k in value: result[k] = decode(value[k])
@@ -311,22 +342,29 @@ static func decode(value):
return value return value
static func valid_encoded(value) -> bool: static func valid_encoded(value) -> bool:
if value is float: return is_finite(value) and absf(value) < 1e28
if value is Dictionary: 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.has("$exact"):
if value.size() != 1 or not value["$exact"] is String: return false if value.size() != 1 or not value["$exact"] is String: return false
var text: String = value["$exact"] var text: String = value["$exact"]
if text.length() > 4096: return false if text.length() > 29: return false
var terms: PackedStringArray = text.split("/") var terms: PackedStringArray = text.split("/")
if terms.size() > 2: return false if terms.size() != 1: return false
var n: String = terms[0].trim_prefix("-") var n: String = terms[0].trim_prefix("-")
if n.is_empty(): return false if n.is_empty(): return false
for ch in n: for ch in n:
if ch < "0" or ch > "9": return false if ch < "0" or ch > "9": return false
if terms.size() == 2: if Big.compare(Big.parse(n), LIMIT) >= 0: return false
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
return true return true
for k in value: for k in value:
if not valid_encoded(value[k]): return false if not valid_encoded(value[k]): return false
+7 -5
View File
@@ -1,9 +1,11 @@
extends RefCounted 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 polarity: int = 1
var numerator: Array = [0] var high: int = 0
var denominator: Array = [1] var low: int = 0
var fraction: float = 0.0
func _to_string() -> String: func _to_string() -> String:
const Big = preload("res://world/owniverse/math/big_integer.gd") var whole: String = str(high) + "%014d" % low if high else str(low)
return ("-" if polarity < 0 else "") + Big.decimal(numerator) + ("/" + Big.decimal(denominator) if denominator != [1] else "") var tail: String = String.num(fraction, 16).trim_prefix("0").trim_suffix(".") if fraction else ""
return ("-" if polarity < 0 else "") + whole + tail
+4 -4
View File
@@ -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 # next integer. Skip expired empty windows without changing the span
# passed to the next generation or the lifetime production integral. # passed to the next generation or the lifetime production integral.
if skip_empty_expired: 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 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 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) 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_end: Variant = E.minimum(created_to, window_end)
var part_amount: Variant = remaining_amount var part_amount: Variant = remaining_amount
if (E.gt(duration, 0) and E.lt(part_end, created_to)): 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): if E.le(E.add(window_end, lifespan), completed_before):
# Retain the original integer allocation and lifetime integral, but # Retain the original integer allocation and lifetime integral, but
# do not allocate thousands of already-dead batch dictionaries. # 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) var until: Variant = E.minimum(to_year, end)
if E.lt(to_year, end): if E.lt(to_year, end):
var carry: Variant = batch.get("withdraw_remainder", 0) 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) consumed = E.floor_value(expected)
batch.withdraw_remainder = E.subtract(expected, consumed) batch.withdraw_remainder = E.subtract(expected, consumed)
batch.amount = E.maximum(0, E.subtract(amount, 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) return E.subtract(year, start)
if E.ge(year, end): if E.ge(year, end):
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5"))) 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: func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
+1 -1
View File
@@ -52,7 +52,7 @@ var speed: int :
state.speed_in_year = E.power(10, E.subtract(state.speed, 1)) state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
func evolute(delta: Variant) -> void: 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 return
var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed) var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed)
if not E.finite(clock.datetime): return if not E.finite(clock.datetime): return
+5 -4
View File
@@ -20,7 +20,7 @@ static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
var order: Array = [] var order: Array = []
var assigned: Variant = 0 var assigned: Variant = 0
for id in requests: 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) result[id] = E.floor_value(expected)
assigned = E.add(assigned, result[id]) assigned = E.add(assigned, result[id])
order.append({"id": id, "rest": E.subtract(expected, 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 var step = 1
for cost in costs: for cost in costs:
if E.le(cost, 0): continue 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) step = E.multiply(E.divide(step, whole_gcd(step, denominator)), denominator)
return step return step
@@ -76,12 +77,12 @@ static func split(remainders: Dictionary, key: String, count: Variant, weights:
var expected: Dictionary = {} var expected: Dictionary = {}
var positive_sum: Variant = 0 var positive_sum: Variant = 0
for id in weights: 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])) positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
var order: Array = [] var order: Array = []
var assigned: Variant = 0 var assigned: Variant = 0
for id in weights: 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) result[id] = E.floor_value(normalized)
assigned = E.add(assigned, result[id]) assigned = E.add(assigned, result[id])
order.append({"id": id, "rest": E.subtract(normalized, result[id])}) order.append({"id": id, "rest": E.subtract(normalized, result[id])})
+13 -8
View File
@@ -1,19 +1,24 @@
extends RefCounted extends RefCounted
const E = preload("res://world/owniverse/math/exact.gd") 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 const TICKS_PER_YEAR: int = 1000000000
static func processed_year(datetime): 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): static func elapsed_years(from_year, to_year):
return E.subtract(to_year, from_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: static func advance(time: Dictionary, delta: float, speed: int) -> Dictionary:
var ticks = E.round_value(E.multiply(time.datetime, TICKS_PER_YEAR)) var whole_years = E.floor_value(time.datetime)
var increment: float = delta * pow(10.0, speed - 1) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0)) 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) var whole: float = roundf(increment) if absf(increment - roundf(increment)) < 1e-6 else floorf(increment)
ticks = E.add(ticks, E.of(whole)) var elapsed = E.add(elapsed_whole, E.divide(E.of(whole), TICKS_PER_YEAR))
var datetime = E.divide(ticks, 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), "ticks": ticks, "tick_remainder": increment - whole} return {"datetime": datetime, "year": processed_year(datetime), "tick_remainder": increment - whole}
+1 -1
View File
@@ -102,7 +102,7 @@ func apply_blast(target: Dictionary, power: Variant, environment: Dictionary) ->
var distribution: Dictionary = QuantityMath.normalize_distribution(weights) var distribution: Dictionary = QuantityMath.normalize_distribution(weights)
# Fractional exposure is deterministic; destruction itself remains whole. # Fractional exposure is deterministic; destruction itself remains whole.
for id in distribution: 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: func uses_short_lived_fast_path(id: String) -> bool:
+7 -5
View File
@@ -6,20 +6,22 @@ func stellar_environment(source: Dictionary) -> Dictionary:
var quantities: Dictionary = world.ledger.physical_amounts(source) var quantities: Dictionary = world.ledger.physical_amounts(source)
# SMBH is a self-group in GI; its quantity stores mass, not object count. # SMBH is a self-group in GI; its quantity stores mass, not object count.
var mass: Variant = source.amount.get("SMBH", 0) 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: for id in quantities:
var entity: OwniverseEntity = world.catalog.entities[id] var entity: OwniverseEntity = world.catalog.entities[id]
if E.contains(["H", "He", "Met", "SMBH"], id): if E.contains(["H", "He", "Met", "SMBH"], id):
mass = E.add(mass, quantities[id]) mass = E.add(mass, quantities[id])
else: else:
mass = E.add(mass, E.multiply(quantities[id], E.maximum(0, entity.mass))) 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.approx(quantities[id]) * maxf(0.0, E.approx(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.voids)) * 1e22
var metallicity: Variant = 0 var metallicity: Variant = 0
if E.gt(mass, 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))))) 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: func modify_world_parameter(key: String, change: Variant) -> void:
+1 -1
View File
@@ -91,7 +91,7 @@ static func _integration(target: Dictionary, events: Variant) -> void:
var chosen: int = rng.randi_range(0, 2) var chosen: int = rng.randi_range(0, 2)
counts[chosen] = E.add(counts[chosen], 1) counts[chosen] = E.add(counts[chosen], 1)
else: 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]) 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[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]) counts[2] = E.subtract(rest, counts[1])
+5
View File
@@ -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://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://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://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="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="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"] [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] [node name="SoundVolume" type="HSlider" parent="Stage/MainMenu/Volume" unique_id=1297986446]
custom_minimum_size = Vector2(444, 96) custom_minimum_size = Vector2(444, 96)
layout_mode = 2 layout_mode = 2
theme_override_icons/grabber = ExtResource("61_bemti")
theme_override_icons/grabber_highlight = ExtResource("61_bemti")
scrollable = false scrollable = false
[node name="MusicVolume" type="HSlider" parent="Stage/MainMenu/Volume" unique_id=729455266] [node name="MusicVolume" type="HSlider" parent="Stage/MainMenu/Volume" unique_id=729455266]
custom_minimum_size = Vector2(444, 96) custom_minimum_size = Vector2(444, 96)
layout_mode = 2 layout_mode = 2
theme_override_icons/grabber = ExtResource("61_bemti")
theme_override_icons/grabber_highlight = ExtResource("61_bemti")
scrollable = false scrollable = false
[node name="Credits" type="TextureButton" parent="Stage/MainMenu" unique_id=44726060] [node name="Credits" type="TextureButton" parent="Stage/MainMenu" unique_id=44726060]