diff --git a/autoload/save_manager.gd b/autoload/save_manager.gd index 8326a21..693723f 100644 --- a/autoload/save_manager.gd +++ b/autoload/save_manager.gd @@ -6,7 +6,7 @@ const E = preload("res://world/owniverse/math/exact.gd") const SETTING_SECTION = "Settings" const SAVE_PATH = "user://settings.cfg" -const FORMAT_VERSION : int = 5 +const FORMAT_VERSION : int = 6 const SimulationTime = preload("res://world/owniverse/simulation_time.gd") const WorldParameters = preload("res://world/owniverse/world_parameters.gd") const STATE_VERSION : String = SimulationTime.STATE_VERSION @@ -348,11 +348,19 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array, en if ((not (saved.ethics is Dictionary)) or (not (saved.civilization_homes is Dictionary))): return "invalid civilization state" for id in saved.ethics: - if (not E.contains(stages, id) or (not _number_map(saved.ethics[id], true))): + if (not E.contains(stages, id) or (not (saved.ethics[id] is Dictionary))): return E.add("invalid state.ethics.", str(id)) + if E.ne(saved.ethics[id].keys().size(), 6): + return E.add("invalid ethic profile size for ", str(id)) + var share_sum: float = 0.0 for ethic in saved.ethics[id]: - if not E.contains(["m", "l", "w", "d", "c", "b"], ethic): + var share: Variant = saved.ethics[id][ethic] + var share_value: float = 0.0 + share if share is int or share is float else -1.0 + if not E.contains(["m", "l", "w", "d", "c", "b"], ethic) or not is_finite(share_value) or share_value < 0.0 or share_value > 1.0: return E.add("invalid ethic ", str(ethic)) + share_sum += share_value + if absf(share_sum - 1.0) > 0.000001: + return E.add("invalid ethic profile total for ", str(id)) for id in saved.civilization_homes: if (not _number_map(saved.civilization_homes[id], true)): return "invalid civilization homes" diff --git a/export_presets.cfg b/export_presets.cfg index c25ce50..4aee7fe 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -1,6 +1,7 @@ [runnable_presets] "Windows Desktop"="Windows Desktop" +Android="Android" [preset.0] diff --git a/tests/android_benchmark.gd b/tests/android_benchmark.gd new file mode 100644 index 0000000..36e20c3 --- /dev/null +++ b/tests/android_benchmark.gd @@ -0,0 +1,110 @@ +extends Control + +const E = preload("res://world/owniverse/math/exact.gd") +const RESULT_PATH := "user://android_benchmark_result.txt" +const FRAME_COUNT := 120 + +var status_label: Label + +func _ready() -> void: + status_label = Label.new() + status_label.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + status_label.add_theme_font_size_override("font_size", 32) + add_child(status_label) + call_deferred("_run") + +func _set_status(message: String) -> void: + status_label.text = message + var file := FileAccess.open(RESULT_PATH, FileAccess.WRITE) + if file != null: + file.store_line(message) + file.flush() + +func _restore(value): + if value is float and value == floorf(value) and absf(value) <= 9007199254740991: + return int(value) + if value is Dictionary: + for key in value: + value[key] = _restore(value[key]) + elif value is Array: + for index in value.size(): + value[index] = _restore(value[index]) + return value + +func _load_payload_threaded() -> Dictionary: + var file := FileAccess.open("res://tests/artifacts/all_objects_speed6.adventure.json", FileAccess.READ) + if file == null: + return {"error": "Cannot open benchmark save: %s" % FileAccess.get_open_error()} + var parser := JSON.new() + var error := parser.parse(file.get_as_text()) + if error != OK: + return {"error": "JSON parse error at line %d: %s" % [parser.get_error_line(), parser.get_error_message()]} + var data = E.decode(_restore(parser.data)) + if not data is Dictionary or not data.has("state"): + return {"error": "Benchmark save has no state"} + return {"payload": data} + +func _run() -> void: + _set_status("Reading and decoding benchmark save on a worker thread…") + var worker := Thread.new() + var start_error := worker.start(Callable(self, "_load_payload_threaded")) + if start_error != OK: + _set_status("ERROR: could not start loader thread (%s)" % start_error) + return + while worker.is_alive(): + await get_tree().process_frame + var loaded: Dictionary = worker.wait_to_finish() + if loaded.has("error"): + _set_status("ERROR: " + str(loaded.error)) + return + _set_status("Save decoded. Initializing UI scene…") + var payload: Dictionary = loaded.payload + var scene: Node = load("res://world/world.tscn").instantiate() + var stage = scene.get_node("Stage") + stage.owniverse.free() + stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd")) + get_tree().root.add_child(scene) + await get_tree().process_frame + stage.owniverse.state = payload.state.duplicate(true) + stage.owniverse.restore_frame_checkpoint(payload.get("simulation", {})) + stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog) + stage.set_owniverse_item_value() + stage.stage_page_scroller.scroll_horizontal = 4 * stage.PAGE_WIDTH + var initial_requested = stage.owniverse.frame_clock.get("datetime", stage.owniverse.state.time.datetime) + var initial_published = stage.owniverse.state.time.datetime + stage.owniverse.profile_enabled = true + stage.measuring = true + _set_status("Running speed 6: 0/%d frames" % FRAME_COUNT) + for frame in FRAME_COUNT: + await get_tree().process_frame + if frame % 5 == 4: + _set_status("Running speed 6: %d/%d frames; commits=%d" % [frame + 1, FRAME_COUNT, stage.owniverse.frame_commits]) + stage.measuring = false + var samples: Array = stage.frame_samples + if samples.is_empty(): + _set_status("ERROR: no rendered frame samples; frames=%d" % stage.frame_samples.size()) + return + var total: float = 0.0 + for sample in samples: + total += sample + samples.sort() + var requested_advance = E.subtract(stage.owniverse.frame_clock.datetime, initial_requested) + var published_advance = E.subtract(stage.owniverse.state.time.datetime, initial_published) + var process_samples: Array = stage.process_samples + process_samples.sort() + var process_total: float = 0.0 + for sample in process_samples: + process_total += sample + var process_summary := "no _process samples" + if not process_samples.is_empty(): + process_summary = "mean=%.2fms p95=%.2fms max=%.2fms samples=%d" % [process_total / process_samples.size(), process_samples[int(ceil(process_samples.size() * 0.95)) - 1], process_samples.back(), process_samples.size()] + var budget: Variant = stage.owniverse.frame_budget + var max_slice_summary: String = "none" + if budget != null: + max_slice_summary = "%.2fms (%s)" % [budget.max_slice_usec / 1000.0, budget.max_slice_phase] + var result := "DONE\nFPS=%.2f mean=%.2fms p95=%.2fms max=%.2fms frames=%d commits=%d\nStage _process CPU: %s\nMax uninterrupted simulation slice: %s\nRequested=%s published=%s throughput=%s\nFinal adaptive step=%s\nProfile=%s" % [1000.0 / (total / samples.size()), total / samples.size(), samples[int(ceil(samples.size() * 0.95)) - 1], samples.back(), samples.size(), stage.owniverse.frame_commits, process_summary, max_slice_summary, str(requested_advance), str(published_advance), str(E.divide(published_advance, requested_advance)), str(stage.owniverse.frame_step_seconds), str(stage.owniverse.profile)] + _set_status(result) + get_tree().quit() diff --git a/tests/android_benchmark.gd.uid b/tests/android_benchmark.gd.uid new file mode 100644 index 0000000..534cb6e --- /dev/null +++ b/tests/android_benchmark.gd.uid @@ -0,0 +1 @@ +uid://dkbyhj8ib0eyf diff --git a/tests/android_benchmark.tscn b/tests/android_benchmark.tscn new file mode 100644 index 0000000..269d9ba --- /dev/null +++ b/tests/android_benchmark.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://tests/android_benchmark.gd" id="1"] + +[node name="AndroidBenchmark" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") diff --git a/tests/artifacts/all_objects_speed6.adventure b/tests/artifacts/all_objects_speed6.adventure index e9e2ead..6a7fab3 100644 Binary files a/tests/artifacts/all_objects_speed6.adventure and b/tests/artifacts/all_objects_speed6.adventure differ diff --git a/tests/artifacts/all_objects_speed6.adventure.json b/tests/artifacts/all_objects_speed6.adventure.json index 8cbb144..942fc6b 100644 --- a/tests/artifacts/all_objects_speed6.adventure.json +++ b/tests/artifacts/all_objects_speed6.adventure.json @@ -1,6 +1,6 @@ { "app_version": "0.7.0", - "format_version": 5, + "format_version": 6, "simulation": { "active_clock": {}, "productions": [], @@ -30440,2996 +30440,356 @@ }, "ethics": { "C0.2": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C0.3": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C0.4": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C0.5": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C0.6": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C0.7": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C0.8": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C0.9": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.0": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.1": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.2": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.3": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.4": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.5": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.6": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.7": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.8": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C1.9": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.0": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.1": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.2": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.3": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.4": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.5": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.6": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.7": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.8": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C2.9": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.0": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.1": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.2": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.3": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.4": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.5": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.6": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.7": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.8": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C3.9": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C4.0": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C4.1": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C4.2": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C4.3": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "C4.4": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 }, "Ct": { - "b": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "c": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "d": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "l": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "m": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - }, - "w": { - "$exact": [ - [ - 0, - 0, - 100 - ], - "0000000000000000", - 1 - ] - } + "b": 0.16666666666666666, + "c": 0.16666666666666666, + "d": 0.16666666666666666, + "l": 0.16666666666666666, + "m": 0.16666666666666666, + "w": 0.16666666666666666 } }, "evolution_remainders": {}, diff --git a/tests/artifacts/all_objects_speed6_results.md b/tests/artifacts/all_objects_speed6_results.md index 5bae56d..e0aa432 100644 --- a/tests/artifacts/all_objects_speed6_results.md +++ b/tests/artifacts/all_objects_speed6_results.md @@ -61,3 +61,34 @@ godot --headless --path Owniverse.GI --script res://tests/create_full_benchmark_ godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- Owniverse.GI/tests/artifacts/all_objects_speed6.adventure.json 0.5 profile godot --path Owniverse.GI --script res://tests/benchmark_full_ui.gd ``` + +## Проверка на Android Note8 (2026-10-06) + +Сборка benchmark установлена отдельным пакетом `com.KIDGameProduction.Owniverse.AndroidBench`, сохранение основной игры не использовалось. Подготовка и декодирование сохранения выполнялись в фоновом потоке; ход и результат записывались в файл приложения. Устройство: Samsung SM-N950F, Android 9. Сценарий: все объекты и цивилизации, скорость 6, 120 кадров (119 интервалов). + +| Метрика | Результат | +|---|---:| +| FPS UI | 1,76 | +| Средний / p95 / максимальный кадр | 567,28 / 817,54 / 1278,33 мс | +| Завершенные транзакции за прогон | 1 | +| Запрошено / опубликовано игрового времени | 1599938,57 / 10000 лет | +| Доля опубликованного от запрошенного | 0,00625 | +| Итоговый динамический шаг | 0,2 с | + +Самый дорогой участок профиля — `generations`: 5,350 с суммарно за единственную завершенную транзакцию; внутри него `civilization_prepare` занял 3,186 с, `wars` — 2,217 с, `civilization_transitions` — 1,034 с. Интервалы вложены, поэтому их нельзя складывать с `generations`. Этот экстремальный сценарий на Note8 не достигает целевых 5 FPS и сильно отстает по игровому времени. Это замер именно полного стресс-сохранения, а не обычного игрового сохранения. + +### Покадровая уступка в расчётах цивилизаций + +Добавлены контрольные точки внутри обработки этик, проигравших в войне, падения популяции и выходов между стадиями. Математика и порядок переходов не меняются. На том же Note8 повторный прогон дал 1,87 FPS против 1,76 FPS до изменения (прирост около 6%). Средний кадр сократился с 567 до 535 мс, но p95 остался 630 мс, максимум — 1281 мс. Это слишком малое улучшение для цели 5 FPS. + +В повторном профиле `generations` заняли 5,166 с, `civilization_prepare` — 3,040 с, войны — 0,985 с, переходы между стадиями — 0,640 с. Профиль не показывает явного трёхкратного ускорения: контрольные точки лучше дробят отдельные подциклы, но главный синхронный участок остается слишком дорогим. Диагностическая сборка для измерения максимального непрерывного участка не завершила прогон на телефоне; при следующем проходе сначала нужно локализовать этот участок, а затем оптимизировать его отдельно. + +Контрольный запуск UI-бенчмарка на Apple M4 Pro после изменения: 21,51 FPS, средний / p95 / максимальный кадр — 46,49 / 89,62 / 107,19 мс. Этот результат подтверждает отсутствие регрессии на компьютере, но не заменяет Android-замер. + +### После перехода этик на доли (2026-10-06) + +Этическое состояние теперь хранит шесть долей `float`, а точное целое население берётся из реестра объектов. Исходы одной стадии сначала собираются по назначениям; доли состояния обновляются после агрегации потоков. Формат сохранения повышен до версии 6; миграции прежних сохранений нет. + +Финальная APK повторно проверена на Samsung SM-N950F, Android 9, тем же полным сценарием и 120 кадрами. Два последовательных прогона дали **14,57 и 15,02 FPS**, средний кадр — **68,61 и 66,58 мс**, p95 — **113,03 и 123,03 мс**, по одной завершённой транзакции из 119 интервалов. Опубликовано 10 000 лет из 647 331 и 634 134 запрошенных; пропускная способность — 0,01545 и 0,01577. В последнем профиле `generations` заняли 5,120 с, `civilization_prepare` — 3,162 с, войны — 1,026 с, переходы — 0,728 с, дрейф этик — 0,474 с. Самый длинный непрерывный участок составил **458,14 мс** в `deep_copy_and_coarsen`, максимальный кадр — 976 мс. Предыдущий одиночный запуск старой benchmark-сборки показал 1,83 FPS, поэтому сравнивать его с этой парой напрямую нельзя; оба финальных прогона выполнены на одной APK и подтверждают результат около 15 FPS. + +На компьютере прошли тесты состояния (70), эволюции (555), оптимизаций симуляции (364), индикаторов (63) и жизни (25). Полный набор всё ещё сообщает о двух отказах в `test_descriptions.gd` при обновлении выбранного описания и смене языка; оба относятся к UI-описаниям, а не к модели этик. В логах Android ошибок GDScript/Godot за прогон не найдено. diff --git a/tests/test_civilization_evolution.gd b/tests/test_civilization_evolution.gd index f36274e..52dc449 100644 --- a/tests/test_civilization_evolution.gd +++ b/tests/test_civilization_evolution.gd @@ -42,18 +42,17 @@ func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}] world.evolution.initialize_life_batches() -func sum_ethics(world, id: String) -> Variant: - var result: Variant = 0 - for n in world.state.ethics.get(id, {}).values(): result = E.add(result, n) - return result - func invariants(world) -> void: var physical: Dictionary = world.ledger.physical_amounts(world.state) for id in world.state.ethics: - check(E.eq(sum_ethics(world, id), physical.get(id, 0)), E.add("Ethics equal physical population for ", id)) + var total: float = 0.0 for n in world.state.ethics[id].values(): - if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount") - check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates") + var share: float = 0.0 + n + if not is_finite(share) or share < 0.0 or share > 1.0: check(false, "Invalid ethic share") + total += share + check(absf(total - 1.0) < 0.000001, E.add("Ethic shares sum to one for ", id)) + check(E.ge(physical.get(id, 0), 0), E.add("Exact physical population remains available for ", id)) + check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates") func run() -> void: var global: Node = root.get_node("Global") diff --git a/tests/test_civilization_state.gd b/tests/test_civilization_state.gd index 361d349..bbcc75b 100644 --- a/tests/test_civilization_state.gd +++ b/tests/test_civilization_state.gd @@ -48,18 +48,23 @@ func run() -> void: world.evolution.initialize_life_batches() check((E.eq(world.state.amount["C2.0"], 12) and E.eq(world.state.binds.RD["C2.0"], 0)), "Old space civilizations detach without loss") check(E.eq(world.state.binds.RD["C1.9"], 3), "Planetary civilizations retain their host") - check(E.eq(world.state.ethics["C0.2"], {"m": 2, "l": 2, "w": 1, "d": 1, "c": 1, "b": 1}), "Migration uses equal ethics and deterministic remainder") - check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.eq(n, 2)), "Bound civilization ethics count physical population") + check(world.state.ethics["C0.2"].values().all(func ( n ) : return E.near(n, 1.0 / 6.0)), "Migration initializes equal ethic shares") + check(world.state.ethics["C2.0"].values().all(func ( n ) : return E.near(n, 1.0 / 6.0)), "Bound civilization ethics are population-independent shares") var before: Dictionary = world.state.duplicate(true) world.evolution.initialize_life_batches() - check(E.eq(world.state, before), "Repeated initialization preserves ethics") + var profiles_stable: bool = true + for ethic_id in before.ethics: + for ethic in before.ethics[ethic_id]: + profiles_stable = profiles_stable and E.near(world.state.ethics[ethic_id][ethic], before.ethics[ethic_id][ethic]) + check(profiles_stable, "Repeated initialization preserves ethics") world.state.ethics["C0.2"] = {"m": 8, "l": 0, "w": 0, "d": 0, "c": 0, "b": 0} world.evolution.initialize_life_batches() - check(E.eq(world.state.ethics["C0.2"].m, 8), "Existing unequal distribution is retained") + check(E.eq(world.state.ethics["C0.2"].m, 1.0), "Existing unequal distribution is retained") world.state.amount["C0.2"] = 3 world.evolution.initialize_life_batches() - check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population") - check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates") + check(E.eq(world.state.ethics["C0.2"].m, 1.0), "Losses retain normalized ethical composition") + check(E.eq(world.ledger.physical_amounts(world.state)["C0.2"], 3), "Exact population remains in the object ledger") + check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates") var restored = fixture() restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state)))) restored.evolution.initialize_life_batches() @@ -117,7 +122,7 @@ func run() -> void: world.evolution.initialize_life_batches() check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent") check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families") - check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates") + check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates") dispose(world) world = fixture() diff --git a/tests/test_coarse_generations.gd b/tests/test_coarse_generations.gd index c9ab984..e4835fb 100644 --- a/tests/test_coarse_generations.gd +++ b/tests/test_coarse_generations.gd @@ -57,7 +57,7 @@ func run(): var before: Dictionary = world.state.duplicate(true) world.evolution.evolute_to_datetime(10000) check(E.eq(world.state, before), "Repeating completed clock does not repeat transitions") - var validation: String = root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) + var validation: String = root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) check(validation.is_empty(), "Coarse state validates: " + validation) world.evolution.evolute_to_datetime(20000) if id in ["C0.0", "C0.1"]: diff --git a/tests/test_exact_math.gd b/tests/test_exact_math.gd index 4f793ae..d13a42b 100644 --- a/tests/test_exact_math.gd +++ b/tests/test_exact_math.gd @@ -94,7 +94,7 @@ func run(): check(str(world.state.auto_star_deficits).length() < 500, "true", "fraction storage stays bounded") print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started)) var saves = root.get_node("SaveManager") - check(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves") + check(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves") for entity in world.catalog.entities.values(): entity.free() world.free() print("Exact math: %d checks, %d failures" % [checks, failures]) diff --git a/tests/test_life.gd b/tests/test_life.gd index 522235a..f49959e 100644 --- a/tests/test_life.gd +++ b/tests/test_life.gd @@ -54,7 +54,7 @@ func run() -> void: check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt") var physical: Dictionary = world.ledger.physical_amounts(world.state) check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "Life conserves planets") - check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates") + check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates") var restored = fixture() restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state)))) world.evolution.evolve_batches(world.state, 11000000) @@ -120,7 +120,7 @@ func run() -> void: world.evolution.evolve_batches(world.state, 2050) check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host") check(E.eq(world.state.binds.RD["C0.0"], 1000), "C0.1 degradation follows Unity") - check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates") + check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates") dispose(world) print(("Life: %d checks, %d failures" % [checks, failures])) quit((0 if E.eq(failures, 0) else 1)) diff --git a/tests/test_simulation_optimizations.gd b/tests/test_simulation_optimizations.gd index 5de3ed8..5a8ecb4 100644 --- a/tests/test_simulation_optimizations.gd +++ b/tests/test_simulation_optimizations.gd @@ -78,30 +78,41 @@ func run(): changed = Civ.coefficients(world, local_rules, id, modifiers) check(E.eq(changed.m.pro, E.divide(E.of("0.375"), E.maximum(1, E.add(E.of("0.375"), d)))), "Coefficient cache follows rule edits") var target: Dictionary = {"ethics": {}} + target.amount = {} + target.binds = {} + target.stats = {"total_amount": {}} + target.amount[id] = 60 + target.stats.total_amount[id] = 60 Civ.State.reconcile(world, target, rules, {id: 60}) - target.ethics[id].m = 100 + target.ethics[id] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0} Civ.State.reconcile(world, target, rules, {id: 60}) - check(E.eq(Civ.total(target.ethics[id]), 60), "Reconcile cache detects changed ethics") + check(E.eq(target.ethics[id].m, 1.0), "Reconcile cache detects changed ethics") Civ.State.reconcile(world, target, rules, {id: 120}) - check(E.eq(Civ.total(target.ethics[id]), 120), "Reconcile cache detects changed population") + check(E.eq(target.ethics[id].m, 1.0), "Reconcile cache detects changed population") + target.amount[id] = 120 Civ.worship(world, target, rules) - target.ethics[id].m = E.add(target.ethics[id].m, 5) - var expected: Variant = 0 + target.ethics[id].m = 0.5 + target.ethics[id].l = 0.5 + var expected: float = 0.0 for stage in rules: if rules[stage].worships: for ethic in target.ethics[stage]: - expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(rules[stage].ethics[ethic].worship))) - check(E.eq(Civ.worship(world, target, rules), expected), "Worship cache detects changed ethics") + var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(rules[stage].ethics[ethic].worship)) + n *= E.approx(target.amount.get(stage, 0)) + expected += n + check(E.near(Civ.worship(world, target, rules), expected), "Worship cache detects changed ethics") var worship_rules: Dictionary = rules.duplicate(true) for stage in worship_rules: if worship_rules[stage].worships: worship_rules[stage].ethics.m.worship = "0.375" - expected = 0 + expected = 0.0 for stage in worship_rules: if worship_rules[stage].worships: for ethic in target.ethics[stage]: - expected = E.add(expected, E.multiply(target.ethics[stage][ethic], E.of(worship_rules[stage].ethics[ethic].worship))) - check(E.eq(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules") + var n: float = (0.0 + target.ethics[stage][ethic]) * E.approx(E.of(worship_rules[stage].ethics[ethic].worship)) + n *= E.approx(target.amount.get(stage, 0)) + expected += n + check(E.near(Civ.worship(world, target, worship_rules), expected), "Worship cache detects changed rules") world.ledger = load("res://tests/fixtures/audit_rounding_ledger.gd").new(world) world.speed = speed for i in 3: world.evolute(1.0 if speed == 1 else 0.1) diff --git a/tests/test_stellar_rules.gd b/tests/test_stellar_rules.gd index 03b2bfb..5fd0836 100644 --- a/tests/test_stellar_rules.gd +++ b/tests/test_stellar_rules.gd @@ -96,7 +96,7 @@ func run() -> void: check((E.eq(world.state.binds.RD.GG, 16) and E.eq(world.state.binds.RD.AB, 120)), "Planet multiplicity applied") check((E.eq(world.state.amount.HP, 0) and E.eq(world.state.stats.total_amount.HP, 1)), "Created planets are bound, counted once") var saves: Node = root.get_node("SaveManager") - check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates") + check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates") restored = fixture() restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state)))) world.stellar.create_planets(world.state, "RD", 37, 0, 0, E.of("0.7")) @@ -118,7 +118,7 @@ func run() -> void: check((E.eq(world.state.amount.RP, 0) and E.eq(world.state.amount["C1.3"], 0)), "Inherited planets never become free") world.evolution.evolve_batches(world.state, E.add(lifespan, width)) check((E.eq(world.state.binds.RD["C1.3"], 0) and E.eq(world.state.binds.TWD["C1.3"], 4)), "No planets left at extinct parent type") - check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates") + check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates") dispose(world) world = fixture() @@ -158,7 +158,7 @@ func run() -> void: check((E.eq(world.state.amount.SN, 0) and E.eq(world.state.amount.LNS, 1)), "Supernova produces a remnant") check(((E.eq(world.state.amount.H, 8) and E.eq(world.state.amount.He, 50)) and E.eq(world.state.amount.Met, 11)), "Supernova returns table resources") check((not world.state.amount.has("blast")), "Supernova dispatches blast output") - check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates") + check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates") dispose(world) world = fixture() diff --git a/tests/test_time.gd b/tests/test_time.gd index 3376e2e..2674192 100644 --- a/tests/test_time.gd +++ b/tests/test_time.gd @@ -51,10 +51,10 @@ func run() -> void: for invalid in [0, E.neg(1), NAN, INF, 1e308]: world.evolute(invalid) check(E.eq(world.state, before), "Invalid/overflow interval rejected") - check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid") + check(E.eq(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid") var invalid_save: Dictionary = world.state.duplicate(true) invalid_save.time.year = E.of("0.25") - check(E.ne(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock") + check(E.ne(saves.validate_data({"format_version": 6, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock") dispose(world) for fps in [30, 60, 144]: diff --git a/tests/test_world_indicators.gd b/tests/test_world_indicators.gd index 217f8cc..a96ba78 100644 --- a/tests/test_world_indicators.gd +++ b/tests/test_world_indicators.gd @@ -91,7 +91,7 @@ func run() -> void: world = fixture() world.state.amount["C0.2"] = 1000000 - world.state.ethics["C0.2"] = {"m": 1000000} + world.state.ethics["C0.2"] = {"m": 1.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0} var baseline: Dictionary = world.state.duplicate(true) var advanced: Dictionary = baseline.duplicate(true) advanced.world_parameters.Agg = 19 @@ -108,7 +108,9 @@ func run() -> void: # Trends use ethical proportions and stage weights, not population magnitude. world = fixture() var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}} - world.state.ethics = {"C2.0": {"d": 100}, "C2.1": {"d": 100}} + world.state.ethics = {"C2.0": {"m": 0.0, "l": 0.0, "w": 0.0, "d": 1.0, "c": 0.0, "b": 0.0}, "C2.1": {"m": 0.0, "l": 0.0, "w": 0.0, "d": 1.0, "c": 0.0, "b": 0.0}} + world.state.amount["C2.1"] = 10 + world.state.binds["RD"] = {"C2.0": 10} var source: Dictionary = world.state.duplicate(true) var single: Dictionary = source.duplicate(true) var split: Dictionary = source.duplicate(true) @@ -117,10 +119,17 @@ func run() -> void: check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights") for key in Parameters.TRENDS: check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key)) - source.ethics["C2.0"].d = 1000000 + source.ethics["C2.0"].d = 1.0 var larger: Dictionary = world.state.duplicate(true) + larger.amount["C2.1"] = 1000000 + larger.binds["RD"]["C2.0"] = 1000000 Parameters.advance(source, larger, rules, 1000000) check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence") + var vacant: Dictionary = world.state.duplicate(true) + vacant.amount["C2.1"] = 0 + vacant.binds["RD"]["C2.0"] = 0 + Parameters.advance(vacant, vacant, rules, 1000000) + check(vacant.trends.values().all(func (value): return E.eq(value, 0)), "Empty saved ethic profiles do not generate trends") world.state.trends.Integration = E.of("0.99") Parameters.advance(source, world.state, rules, 100000000) check(E.eq(world.state.civilization_stats["trend:Integration"], 2), "Long steps process every crossed threshold") @@ -133,7 +142,7 @@ func run() -> void: Parameters.advance(source, world.state, rules, E.of("1.0e12")) check((E.ge(world.state.trends.Integration, 0) and E.lt(world.state.trends.Integration, 1)), "Very large intervals finish with a valid trend fraction") check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale") - var payload: Dictionary = {"format_version": 5, "app_version": "test", "ui": {"current_page": 1}, "state": world.state} + var payload: Dictionary = {"format_version": 6, "app_version": "test", "ui": {"current_page": 1}, "state": world.state} var manager: Node = root.get_node("SaveManager") check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation") var old: Dictionary = payload.duplicate(true) diff --git a/world/owniverse/civilization_evolution.gd b/world/owniverse/civilization_evolution.gd index eee0648..e1178b5 100644 --- a/world/owniverse/civilization_evolution.gd +++ b/world/owniverse/civilization_evolution.gd @@ -58,7 +58,7 @@ static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictio weights.append(E.maximum(0, E.subtract(E.add(common, E.multiply(contributions[ETHICS.find(ethic)], 951 if colony else 195)), E.multiply(contributions[ETHICS.find(OPPOSITE[ethic])], 5 if colony else 1)))) if E.le(QuantityMath.sum_array(weights), 0): return pop.duplicate() if not target.has("civilization_remainders"): target.civilization_remainders = {} - return QuantityMath.split_arrays(target.civilization_remainders, key, population, ETHICS, weights, 0) + return QuantityMath.split_arrays(target.civilization_remainders, key, population, ETHICS, weights, 0, true) static func diplomacy_rates(rows: Dictionary) -> Array: var signature: String = "" @@ -188,18 +188,20 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop # Keeping the summation order preserves float fractional rounding exactly. static func worship(world, target: Dictionary, rules: Dictionary) -> Variant: var result: Variant = 0 + var physical: Dictionary = world.ledger.physical_amounts(target) for id in rules: if not rules[id].worships: continue - var pop: Dictionary = target.ethics[id] + var pop: Dictionary = State.normalize_profile(target.ethics.get(id, {})) var coefficients: Dictionary = {} for ethic in pop: coefficients[ethic] = rules[id].ethics[ethic].worship var cached: Dictionary = world.ledger.worship_products.get(id, {}) - if cached.is_empty() or not E.eq(cached.population, pop) or not E.eq(cached.coefficients, coefficients): - var products: Dictionary = {} - for ethic in pop: products[ethic] = E.multiply(pop[ethic], E.of(coefficients[ethic])) - cached = {"population": pop.duplicate(), "coefficients": coefficients, "products": products} + var count: Variant = physical.get(id, 0) + if cached.is_empty() or not E.eq(cached.population, pop) or not E.eq(cached.coefficients, coefficients) or not E.eq(cached.count, count): + var worship_rate: float = 0.0 + for ethic in pop: worship_rate += (0.0 + pop[ethic]) * E.approx(E.of(coefficients[ethic])) + cached = {"population": pop.duplicate(), "coefficients": coefficients, "count": count, "product": E.multiply(count, worship_rate)} world.ledger.worship_products[id] = cached - for ethic in pop: result = E.add(result, cached.products[ethic]) + result = E.add(result, cached.product) return result static func coefficients(world, rules: Dictionary, id: String, modifiers: Dictionary) -> Dictionary: @@ -236,8 +238,13 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian for id in ids: reconcile_ids.append(id) if rules[id].fights and not reconcile_ids.has(rules[id].previous): reconcile_ids.append(rules[id].previous) + if budget_timer != null: budget_timer.call("set_phase", "civilization_reconcile") State.reconcile(world, target, rules, physical, reconcile_ids) - var remaining: Dictionary = target.ethics.duplicate() + if physical.is_empty(): physical = world.ledger.physical_amounts(target) + var remaining: Dictionary = {} + for id in reconcile_ids: + if rules.has(id): + remaining[id] = State.materialize_profile(target, id, target.ethics.get(id, {}), physical.get(id, 0)) var copied: Dictionary = {} var plans: Dictionary = {} var colonies: Array = [] @@ -247,6 +254,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian if (not target.has("civilization_stats")): target.civilization_stats = {} ids.sort() for id in ids: + if budget_timer != null: budget_timer.call("set_phase", "civilization_stage:" + id) if budget_timer != null and not await budget_timer.pause(): return {} if ((not rules.has(id)) or expired[id].is_empty()): continue var plan: Dictionary = new_plan(id, year) @@ -265,6 +273,8 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian var settlers: Dictionary = {} var stage_rates: Dictionary = coefficients(world, rules, id, modifiers) for ethic in triggered: + if budget_timer != null: budget_timer.call("set_phase", "civilization_ethic:" + id) + if budget_timer != null and not await budget_timer.pause(): return {} var probability: Dictionary = stage_rates[ethic] var prefix: String = id + ":" + ethic var pro: Variant = rate(target, prefix + ":pro", triggered[ethic], probability.pro) @@ -277,11 +287,15 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg)) var profile_transitions: int = world.profile_start() add(triggered, progress(world, target, rules, budget, plan, successes)) + if budget_timer != null and not await budget_timer.pause(): return {} fall(target, plan, rules, failures) world.profile_end("civilization_transitions", profile_transitions) if rules[id].fights: var profile_wars: int = world.profile_start() - for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic]) + for ethic in warriors: + if budget_timer != null: budget_timer.call("set_phase", "civilization_war:" + id) + if budget_timer != null and not await budget_timer.pause(): return {} + warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic]) var warriors_total: Variant = total(warriors) var triggered_total: Variant = total(triggered) var remaining_total: Variant = total(remaining[id]) @@ -289,30 +303,36 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian warriors = QuantityMath.allocate_whole_with_total(warriors, wars, warriors_total) for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic]) triggered_total = E.subtract(triggered_total, wars) - var outcomes: Dictionary = split(target, E.add(id, ":wars"), wars, {"deg": 1, "pro25": 2, "pro50": 3, "pro75": 3}) - var win_expected: Variant = E.add(E.add(E.multiply(outcomes.get("pro25", 0), E.of("0.25")), E.multiply(outcomes.get("pro50", 0), E.of("0.5"))), E.multiply(outcomes.get("pro75", 0), E.of("0.75"))) - var loss_expected: Variant = E.add(E.add(E.add(E.multiply(outcomes.get("deg", 0), 2), outcomes.get("pro25", 0)), E.multiply(outcomes.get("pro50", 0), E.of("1.5"))), E.multiply(outcomes.get("pro75", 0), 2)) - var wins_requested: Variant = rate(target, E.add(id, ":wins"), win_expected, 1) + # Integrate the four equally weighted battle outcomes directly. + # Their expected coefficients are wins=17/36, deaths=1/9, + # total losses=29/18, and collateral=40/3 per battle. Rate + # remainders preserve fractional results between generations. + var wins_requested: Variant = rate(target, E.add(id, ":wins"), wars, E.of("17/36")) var winners: Dictionary = take(world, warriors, wins_requested, wars) var winner_total: Variant = E.floor_value(E.minimum(E.maximum(0, wins_requested), wars)) warriors_total = E.subtract(wars, winner_total) - var losses_requested: Variant = rate(target, E.add(id, ":attackers_lost"), E.divide(outcomes.get("deg", 0), order), 1) + var deaths_expected: Variant = E.divide(wars, 9) + var losses_requested: Variant = rate(target, E.add(id, ":attackers_lost"), E.divide(deaths_expected, order), 1) var attackers_lost: Dictionary = take(world, warriors, losses_requested, warriors_total) var attackers_lost_total: Variant = E.floor_value(E.minimum(E.maximum(0, losses_requested), warriors_total)) warriors_total = E.subtract(warriors_total, attackers_lost_total) add(triggered, progress(world, target, rules, budget, plan, winners, winner_total)) triggered_total = total(triggered) + if budget_timer != null and not await budget_timer.pause(): return {} fall(target, plan, rules, attackers_lost) remaining_total = total(remaining[id]) - var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(loss_expected, order), 1), E.add(triggered_total, remaining_total)) + var losses_expected: Variant = E.multiply(wars, E.of("29/18")) + var losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(losses_expected, order), 1), E.add(triggered_total, remaining_total)) var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": triggered_total, "later": remaining_total}, losses, E.add(triggered_total, remaining_total)) var victims: Dictionary = take(world, triggered, sides.due, triggered_total) triggered_total = E.subtract(triggered_total, sides.due) add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total)) + if budget_timer != null and not await budget_timer.pause(): return {} fall(target, plan, rules, victims) add(triggered, warriors) triggered_total = E.add(triggered_total, warriors_total) - var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide((E.add(E.multiply(outcomes.get("pro50", 0), 10), E.multiply(outcomes.get("pro75", 0), 30))), order), 1) + var damage_expected: Variant = E.multiply(wars, E.of("40/3")) + var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide(damage_expected, order), 1) collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage) target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars) world.profile_end("wars", profile_wars) @@ -333,15 +353,19 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian var sides: Dictionary = QuantityMath.allocate_whole_with_total({"due": retry_total, "later": remaining_total}, amount, E.add(retry_total, remaining_total)) var victims: Dictionary = take(world, retry, sides.due, retry_total) add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year, remaining_total)) + if budget_timer != null and not await budget_timer.pause(): return {} fall(target, plan, rules, victims) target.civilization_stats.collateral = E.add(target.civilization_stats.get("collateral", 0), total(victims)) var incoming: Dictionary = {} var planetary: Dictionary = {} for id in plans: + if budget_timer != null: budget_timer.call("set_phase", "civilization_products:" + id) if budget_timer != null and not await budget_timer.pause(): return {} var plan: Dictionary = plans[id] var products: Dictionary = {} for destination in plan.outputs: + if budget_timer != null: budget_timer.call("set_phase", "civilization_drift:" + id) + if budget_timer != null and not await budget_timer.pause(): return {} var pop: Dictionary = plan.outputs[destination] var count: Variant = total(pop) if rules.has(destination): @@ -356,7 +380,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: Varian planetary[destination] = E.add(planetary.get(destination, 0), count) expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}] world.profile_end("civilization_prepare", profile_started) - return {"active": true, "remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies} + return {"active": true, "remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies, "base_physical": physical.duplicate()} static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary, budget_timer: RefCounted = null) -> void: var profile_started: int = world.profile_start() @@ -366,10 +390,15 @@ static func finish(world, target: Dictionary, transaction: Dictionary, year: Var target.civilization_stats.worship = worship(world, target, rules) world.profile_end("civilization_finish", profile_started) return - target.ethics = transaction.remaining - for id in transaction.incoming: - if (not target.ethics.has(id)): target.ethics[id] = {} - add(target.ethics[id], transaction.incoming[id]) + var remaining: Dictionary = transaction.remaining + var incoming: Dictionary = transaction.incoming + for id in remaining: + var counts: Dictionary = remaining[id].duplicate() + add(counts, incoming.get(id, {})) + target.ethics[id] = State.profile_from_counts(counts) + for id in incoming: + if not remaining.has(id): + target.ethics[id] = State.merge_profile(target.ethics.get(id, {}), transaction.base_physical.get(id, 0), incoming[id]) # Space civilizations that fall back to planetary stages need a stellar host. var physical: Dictionary = world.ledger.physical_amounts(target) var hosts: Dictionary = {} @@ -399,6 +428,16 @@ static func colonize(world, target: Dictionary, colonies: Array, year: Variant, var requests: Dictionary = {} for i in colonies.size(): requests[str(i)] = total(colonies[i].population) var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock)) + var additions: Dictionary = {} + var credit_events: Array = [] + var batch_additions: Dictionary = {} + var debits: Dictionary = {} + var slots_by_resource: Dictionary = {} + for id in stock: + var slots: Dictionary = {"": target.amount.get(id, 0)} + for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0) + slots_by_resource[id] = slots + debits[id] = {} for i in colonies.size(): if budget_timer != null and not await budget_timer.pause(): return var colony: Dictionary = colonies[i] @@ -409,21 +448,40 @@ static func colonize(world, target: Dictionary, colonies: Array, year: Variant, for id in targets: var amount: Variant = targets[id] if E.le(amount, 0): continue - var slots: Dictionary = {"": target.amount.get(id, 0)} - for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0) + var slots: Dictionary = slots_by_resource[id] var taken: Dictionary = QuantityMath.allocate_whole(slots, amount) - world.batches.scale_batches(target, id, E.subtract(1, E.divide(amount, stock[id]))) stock[id] = E.subtract(stock[id], amount) for owner in taken: if E.le(taken[owner], 0): continue - if E.eq(owner, ""): target.amount[id] = E.subtract(target.amount[id], taken[owner]) - else: target.binds[owner][id] = E.subtract(target.binds[owner][id], taken[owner]) - var created: Variant = world.ledger.credit(target, colony.stage, taken[owner]) - if E.ne(owner, ""): - target.amount[colony.stage] = E.subtract(target.amount[colony.stage], created) - target.binds[owner][colony.stage] = E.add(target.binds[owner].get(colony.stage, 0), created) - world.batches.append_batch(target, colony.stage, amount, year, year) - if (not target.ethics.has(colony.stage)): target.ethics[colony.stage] = {} - add(target.ethics[colony.stage], population) + slots[owner] = E.subtract(slots.get(owner, 0), taken[owner]) + debits[id][owner] = E.add(debits[id].get(owner, 0), taken[owner]) + credit_events.append({"stage": colony.stage, "owner": owner, "amount": taken[owner]}) + batch_additions[colony.stage] = E.add(batch_additions.get(colony.stage, 0), taken[owner]) + if not additions.has(colony.stage): additions[colony.stage] = {} + add(additions[colony.stage], population) target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count) + # Commit each resource and owner once after all colonies have consumed their + # sequentially allocated shares. This keeps allocation order exact while + # avoiding repeated batch scaling and ledger mutations in the inner loop. + for id in debits: + var removed: Variant = E.subtract(physical.get(id, 0), stock[id]) + if E.gt(physical.get(id, 0), 0): + world.batches.scale_batches(target, id, E.subtract(1, E.divide(removed, physical[id]))) + for owner in debits[id]: + if E.eq(owner, ""): + target.amount[id] = E.subtract(target.amount.get(id, 0), debits[id][owner]) + elif target.binds.has(owner): + target.binds[owner][id] = E.subtract(target.binds[owner].get(id, 0), debits[id][owner]) + for event: Dictionary in credit_events: + var stage: String = event.stage + var owner: String = event.owner + var created: Variant = world.ledger.credit(target, stage, event.amount) + if E.ne(owner, ""): + target.amount[stage] = E.subtract(target.amount[stage], created) + if not target.binds.has(owner): target.binds[owner] = {} + target.binds[owner][stage] = E.add(target.binds[owner].get(stage, 0), created) + for stage in batch_additions: + world.batches.append_batch(target, stage, batch_additions[stage], year, year) + for id in additions: + target.ethics[id] = State.merge_profile(target.ethics.get(id, {}), physical.get(id, 0), additions[id]) world.profile_end("colonization", profile_colonies) diff --git a/world/owniverse/civilization_state.gd b/world/owniverse/civilization_state.gd index 9d67a22..a9de1c4 100644 --- a/world/owniverse/civilization_state.gd +++ b/world/owniverse/civilization_state.gd @@ -5,11 +5,68 @@ const QuantityMath = preload("res://world/owniverse/quantity_math.gd") const ETHICS = ["m", "l", "w", "d", "c", "b"] +static func empty_profile() -> Dictionary: + var result: Dictionary = {} + for ethic in ETHICS: result[ethic] = 1.0 / ETHICS.size() + return result + +static func normalize_profile(profile: Dictionary) -> Dictionary: + var result: Dictionary = {} + var total: float = 0.0 + for ethic in ETHICS: + var value: float = maxf(0.0, E.approx(profile.get(ethic, 0.0))) + result[ethic] = value + total += value + if not is_finite(total) or total <= 0.0: return empty_profile() + if profile.size() == ETHICS.size() and absf(total - 1.0) <= 0.000000000001: + return result + var assigned: float = 0.0 + for i in ETHICS.size() - 1: + var ethic: String = ETHICS[i] + result[ethic] = (0.0 + result[ethic]) / total + assigned += result[ethic] + result[ETHICS.back()] = maxf(0.0, 1.0 - assigned) + return result + +static func profile_from_counts(population: Dictionary) -> Dictionary: + var total: Variant = QuantityMath.sum(population) + if E.le(total, 0): return empty_profile() + var profile: Dictionary = {} + for ethic in ETHICS: + profile[ethic] = E.approx(E.divide(population.get(ethic, 0), total)) + return normalize_profile(profile) + +static func materialize_profile(target: Dictionary, id: String, profile: Dictionary, count: Variant) -> Dictionary: + var normalized: Dictionary = normalize_profile(profile) + if not target.has("civilization_remainders"): target.civilization_remainders = {} + var shares: Array = [] + for ethic in ETHICS: shares.append(normalized[ethic]) + var result: Dictionary = QuantityMath.split_profile(target.civilization_remainders, "profile:" + id, count, ETHICS, shares) + for ethic in ETHICS: + if not result.has(ethic): result[ethic] = 0 + return result + +static func merge_profile(base: Dictionary, base_count: Variant, incoming: Dictionary) -> Dictionary: + var incoming_count: Variant = QuantityMath.sum(incoming) + return merge_profiles(base, base_count, profile_from_counts(incoming), incoming_count) + +static func merge_profiles(base: Dictionary, base_count: Variant, incoming_profile: Dictionary, incoming_count: Variant) -> Dictionary: + var total: Variant = E.add(base_count, incoming_count) + if E.le(total, 0): return empty_profile() + var base_ratio: float = E.approx(E.divide(base_count, total)) if E.gt(base_count, 0) else 0.0 + var incoming_ratio: float = E.approx(E.divide(incoming_count, total)) if E.gt(incoming_count, 0) else 0.0 + var normalized_base: Dictionary = normalize_profile(base) + var result: Dictionary = {} + for ethic in ETHICS: + var incoming_share: float = E.approx(incoming_profile.get(ethic, 0.0)) if E.gt(incoming_count, 0) else 0.0 + result[ethic] = (0.0 + normalized_base[ethic]) * base_ratio + incoming_share * incoming_ratio + return normalize_profile(result) + static func is_space_civilization(id: String, rules: Dictionary) -> bool: return (rules.has(id) and E.ne(str(rules[id].home), "")) -# Preserve any existing ethics. New populations start equally distributed; -# reductions (including blasts) are proportional and never leave negative counts. +# Keep one six-value float profile per stage. Absolute population remains in +# the exact physical object ledger and is materialized only when a transition needs it. static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}, only_ids: Array = []) -> void: if (not target.has("ethics")): target.ethics = {} @@ -19,22 +76,10 @@ static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Di for id in ids: if not rules.has(id): continue var count: Variant = physical.get(id, 0) - var current: Dictionary = target.ethics.get(id, {}) + var current: Dictionary = normalize_profile(target.ethics.get(id, {})) var cached: Dictionary = world.ledger.reconciled_ethics.get(id, {}) if not cached.is_empty() and E.eq(cached.count, count) and E.eq(cached.population, current): continue - var total: Variant = 0 - for ethic in ETHICS: - current[ethic] = E.maximum(0, current.get(ethic, 0)) - total = E.add(total, current[ethic]) - if E.gt(total, count): - current = QuantityMath.allocate_whole(current, count) - elif E.lt(total, count): - var missing: Variant = E.subtract(count, total) - var base: Variant = E.floor_value(E.divide(missing, ETHICS.size())) - var rest: int = E.integer(E.subtract(missing, E.multiply(base, ETHICS.size()))) - for i in ETHICS.size(): - current[ETHICS[i]] = E.add(current[ETHICS[i]], E.add(base, ((1 if E.lt(i, rest) else 0)))) target.ethics[id] = current world.ledger.reconciled_ethics[id] = {"count": count, "population": current.duplicate()} @@ -115,6 +160,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: detach_space_civilizations(target, rules) var physical: Dictionary = world.ledger.physical_amounts(target) reconcile(world, target, rules, physical) + var ethical_totals: Dictionary = physical.duplicate() var budget: Dictionary = stellar_budget(world, target, rules, physical) var remaining: Dictionary = budget.capacity.duplicate() var remaining_homes: Dictionary = budget.habitats.capacity.duplicate() @@ -141,12 +187,13 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: target.amount[id] = supported world.ledger.credit(target, previous, excess) world.batches.append_batch(target, previous, excess, year, year) - var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess) - if (not target.ethics.has(previous)): - target.ethics[previous] = {} - for ethic in ETHICS: - target.ethics[id][ethic] = E.subtract(target.ethics[id][ethic], removed[ethic]) - target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic]) + var previous_count: Variant = ethical_totals.get(previous, physical.get(previous, target.amount.get(previous, 0))) + var previous_profile: Dictionary = target.ethics.get(previous, empty_profile()) + var source_profile: Dictionary = target.ethics.get(id, empty_profile()) + target.ethics[previous] = merge_profiles(previous_profile, previous_count, source_profile, excess) + ethical_totals[id] = supported + ethical_totals[previous] = E.add(previous_count, excess) + # Capacity losses are proportional, so the surviving source keeps its profile. if E.eq(previous, "C1.9"): var stars: Dictionary = {} var current_physical: Dictionary = world.ledger.physical_amounts(target) diff --git a/world/owniverse/evolution_coordinator.gd b/world/owniverse/evolution_coordinator.gd index 793fb9d..8a18b9c 100644 --- a/world/owniverse/evolution_coordinator.gd +++ b/world/owniverse/evolution_coordinator.gd @@ -30,10 +30,12 @@ func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefC # Fractional frame updates cannot change capacity or populations. var processed_year: Variant = SimulationTime.processed_year(datetime) if E.gt(processed_year, world.state.time.year): + if budget != null: budget.call("set_phase", "initialize_life_batches") var profile_initialize: int = world.profile_start() initialize_life_batches() world.profile_end("initialize", profile_initialize) var previous: Dictionary = world.state + if budget != null: budget.call("set_phase", "deep_copy_and_coarsen") var next: Dictionary = previous.duplicate_deep() next.version = SimulationTime.STATE_VERSION next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT) @@ -87,6 +89,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di # Apply direct completions at the original next-generation boundary, so # binding transfers, the blast environment and capacity checks keep order. if budget != null and not await budget.pause(): return false + if budget != null: budget.call("set_phase", "generation_setup_and_withdrawal") var completed_now: Dictionary = completed.duplicate() completed.clear() if context.is_empty(): @@ -132,7 +135,9 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di var fractions: Dictionary = {} var outputs: Dictionary = {} var current_quantities: Dictionary = quantities + if budget != null: budget.call("set_phase", "civilization_stellar_budget") var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities) + if budget != null: budget.call("set_phase", "civilization_prepare") var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget, expired_totals) if budget != null and budget.cancelled: return false var profile_products: int = world.profile_start() @@ -166,6 +171,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di affected[id] = true for held in target.binds.get(id, {}): affected[held] = true for successor in outputs[id]: affected[successor] = true + if budget != null: budget.call("set_phase", "transfer_evolution") var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs) var new_owners: Dictionary = {} @@ -177,6 +183,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di # blast retain the original snapshot before crediting newborn populations. var environment: Dictionary = world.environment.stellar_environment(target) if E.gt(blast_power, 0) else {} var resolution: Variant = world.batches.temporal_resolution(target) + if budget != null: budget.call("set_phase", "transfer_and_newborns") for id in expired: for part : Dictionary in expired[id]: for successor in part.products: @@ -214,6 +221,7 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di world.profile_end("newborn_batches", profile_newborn) new_owners[successor] = E.add(new_owners.get(successor, 0), amount) world.ledger.accept_transfers(target, incoming) + if budget != null: budget.call("set_phase", "finish_and_capacity") world.ledger.bind_new_owners(target, new_owners) for owner in new_owners: for resource in world.catalog.entities[owner].bind: diff --git a/world/owniverse/frame_budget.gd b/world/owniverse/frame_budget.gd index 5eeb77e..d44942a 100644 --- a/world/owniverse/frame_budget.gd +++ b/world/owniverse/frame_budget.gd @@ -5,18 +5,32 @@ var slice_usec: int = 40000 var deadline: int var cancelled: bool = false var waiting_usec: int = 0 +var checkpoint_usec: int = 0 +var max_slice_usec: int = 0 +var max_slice_phase: String = "startup" +var phase: String = "startup" func _init(scene_tree: SceneTree, milliseconds: int = 40): tree = scene_tree slice_usec = milliseconds * 1000 deadline = Time.get_ticks_usec() + slice_usec + checkpoint_usec = deadline - slice_usec + +func set_phase(value: String) -> void: + phase = value func pause() -> bool: var keep_alive: RefCounted = self if cancelled: return false + var now: int = Time.get_ticks_usec() + var work_usec: int = now - checkpoint_usec + if work_usec > max_slice_usec: + max_slice_usec = work_usec + max_slice_phase = phase if Time.get_ticks_usec() >= deadline: var waiting_from: int = Time.get_ticks_usec() await tree.process_frame keep_alive.waiting_usec += Time.get_ticks_usec() - waiting_from deadline = Time.get_ticks_usec() + slice_usec + checkpoint_usec = Time.get_ticks_usec() return not keep_alive.cancelled diff --git a/world/owniverse/quantity_math.gd b/world/owniverse/quantity_math.gd index dc11599..1d5bda7 100644 --- a/world/owniverse/quantity_math.gd +++ b/world/owniverse/quantity_math.gd @@ -123,11 +123,69 @@ static func sum(pop: Dictionary) -> Variant: static var _split_keys: Dictionary = {} +## Split a whole population using normalized float shares. Quantizing the six +## shares to 15 decimal places matches their stored float precision, while the +## integer denominator avoids a second expensive exact normalization pass. +static func split_profile(remainders: Dictionary, key: String, count: Variant, ids: Array, shares: Array) -> Dictionary: + var result: Dictionary = {} + if E.le(count, 0): return result + assert(E.eq(count, E.floor_value(count)) and ids.size() == shares.size() and not ids.is_empty()) + const SCALE: int = 1000000000000000 + var weights: Array = [] + var assigned_weight: int = 0 + for i in ids.size() - 1: + var weight: int = clampi(roundi(clampf(float(shares[i]), 0.0, 1.0) * SCALE), 0, SCALE) + weights.append(weight) + assigned_weight += weight + var last_weight: int = SCALE - assigned_weight + if last_weight < 0: + var excess: int = -last_weight + for i in range(weights.size() - 1, -1, -1): + var reduction: int = mini(excess, weights[i]) + weights[i] -= reduction + excess -= reduction + if excess == 0: break + last_weight = 0 + weights.append(last_weight) + var cached: Dictionary = _split_keys.get(key, {}) + if cached.is_empty() or cached.ids != ids: + var keys: Array = [] + for id in ids: keys.append(key + ":" + str(id)) + if _split_keys.size() >= 512: _split_keys.clear() + cached = {"ids": ids.duplicate(), "keys": keys} + _split_keys[key] = cached + var expected: Array = E.mul_div_many(count, weights, SCALE) + var rests: Array = [] + var order: Array = [] + var total: Variant = 0 + for i in ids.size(): + expected[i] = E.add(expected[i], remainders.get(cached.keys[i], 0)) + result[ids[i]] = E.floor_value(E.maximum(0, expected[i])) + total = E.add(total, result[ids[i]]) + rests.append(E.subtract(expected[i], result[ids[i]])) + order.append(i) + order.sort_custom(func(a: int, b: int) -> bool: return E.gt(rests[a], rests[b]) if E.ne(rests[a], rests[b]) else str(ids[a]) < str(ids[b])) + if E.lt(total, count): + for i in order: + if E.ge(total, count): break + result[ids[i]] = E.add(result[ids[i]], 1) + total = E.add(total, 1) + elif E.gt(total, count): + order.reverse() + for i in order: + if E.le(total, count): break + if E.le(result[ids[i]], 0): continue + result[ids[i]] = E.subtract(result[ids[i]], 1) + total = E.subtract(total, 1) + for i in ids.size(): + remainders[cached.keys[i]] = E.clamp_value(E.subtract(expected[i], result[ids[i]]), E.neg(1), 1) + return result + static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary: if E.le(count, 0): return {} return split_arrays(remainders, key, count, weights.keys(), weights.values()) -static func split_arrays(remainders: Dictionary, key: String, count: Variant, ids: Array, weights: Array, population_factor: int = 0) -> Dictionary: +static func split_arrays(remainders: Dictionary, key: String, count: Variant, ids: Array, weights: Array, population_factor: int = 0, skip_conserved_identity: bool = false) -> Dictionary: var result: Dictionary = {} if E.le(count, 0): return result var sum_weights: Variant = 0 @@ -177,7 +235,7 @@ static func split_arrays(remainders: Dictionary, key: String, count: Variant, id # Fractional identity ratios still round-trip through parts/make in the # original algorithm. Retain them to preserve ties and saved errors exactly. var normalized: Array = positives - if E.gt(positive_sum, 0) and (E.ne(positive_sum, count) or not whole_quotas): + if E.gt(positive_sum, 0) and (E.ne(positive_sum, count) or (not whole_quotas and not (skip_conserved_identity and E.eq(positive_sum, count)))): normalized = E.mul_div_many(count, positives, positive_sum) var order: Array = [] var rests: Array = [] diff --git a/world/owniverse/world_parameters.gd b/world/owniverse/world_parameters.gd index b7d8914..7a0fc6e 100644 --- a/world/owniverse/world_parameters.gd +++ b/world/owniverse/world_parameters.gd @@ -62,16 +62,26 @@ static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, for id in rules: if E.ne(str(rules[id].home), ""): stages.append(id) stages.sort() + # Profiles remain in the save after a stage becomes empty. Only living + # populations (including populations bound to a host object) contribute. + var populated: Dictionary = {} + var amounts: Dictionary = previous.get("amount", {}) + for id in stages: + if E.gt(amounts.get(id, 0), 0): populated[id] = true + for owner in previous.get("binds", {}): + for id in previous.binds[owner]: + if rules.has(id) and E.ne(str(rules[id].home), "") and E.gt(previous.binds[owner][id], 0): + populated[id] = true var weight: Variant = 1 for id in stages: + if not populated.has(id): + weight = E.multiply(weight, 2) + continue var population: Dictionary = previous.get("ethics", {}).get(id, {}) - var count: Variant = 0 - for n in population.values(): count = E.add(count, n) - if E.gt(count, 0): - for ethic in population: - var proportion: Variant = E.divide(population[ethic], count) - for i in TRENDS.size(): - rates[i] = E.add(rates[i], E.multiply(E.multiply(proportion, WEIGHTS[ethic][i]), weight)) + for ethic in population: + var proportion: float = maxf(0.0, float(population[ethic])) + for i in TRENDS.size(): + rates[i] = E.add(rates[i], E.multiply(E.multiply(proportion, WEIGHTS[ethic][i]), weight)) weight = E.multiply(weight, 2) for i in TRENDS.size(): var name: String = TRENDS[i]