Refactor Owniverse modules for quantity mathematics and entity catalog
- Introduced `quantity_math.gd` for arithmetic operations including allocation, normalization, and accumulation of quantities without scene tree dependencies. - Created `entity_catalog.gd` to manage entity definitions, groups, and event indexes, ensuring separation from simulation state and UI. - Implemented `item_presentation.gd` for read-only projections of simulation state, handling amounts and unlock conditions for UI elements. - Updated `civilization_evolution.gd` and `civilization_state.gd` to utilize new quantity math functions, improving code clarity and maintainability. - Enhanced tests in `test_quantity_catalog.gd` to validate new functionality and ensure correctness of mathematical operations. - Revised documentation to reflect changes in module structure and functionality.
This commit is contained in:
+3
-3
@@ -66,15 +66,15 @@ func format_number(value: float, digits: int = 0) -> String:
|
||||
var fixed: String = "%.*f" % [digits, value]
|
||||
var parts: PackedStringArray = fixed.split(".", true)
|
||||
var integer: String = parts[0]
|
||||
var sign: String = ""
|
||||
var sign_prefix: String = ""
|
||||
if integer.begins_with("-") or integer.begins_with("+"):
|
||||
sign = integer.left(1)
|
||||
sign_prefix = integer.left(1)
|
||||
integer = integer.substr(1)
|
||||
var grouped: String = ""
|
||||
while integer.length() > 3:
|
||||
grouped = " " + integer.right(3) + grouped
|
||||
integer = integer.left(integer.length() - 3)
|
||||
grouped = sign + integer + grouped
|
||||
grouped = sign_prefix + integer + grouped
|
||||
return grouped + ("." + parts[1] if parts.size() > 1 else "")
|
||||
|
||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
||||
|
||||
+9
-38
@@ -116,25 +116,11 @@ func set_owniverse_item_value() -> void:
|
||||
|
||||
|
||||
func _unlock_items():
|
||||
if locked_items.size() == 0: return
|
||||
var _new_locked_items = []
|
||||
var display_totals: Dictionary = owniverse.get_item_amounts(true)
|
||||
for _item_id in locked_items:
|
||||
#print(_item_id ' | ', _entity.unlock)
|
||||
var _flag_to_unlock = true
|
||||
for _elem in owniverse.entities[_item_id].unlock:
|
||||
if maxf(owniverse.state.stats.total_amount.get(_elem, 0.0), display_totals.get(_elem, 0.0)) < owniverse.entities[_item_id].unlock[_elem]:
|
||||
_flag_to_unlock = false
|
||||
break
|
||||
|
||||
if _flag_to_unlock:
|
||||
for item_id in [_item_id, owniverse.entities[_item_id].count_as]:
|
||||
if owniverse_items.has(item_id):
|
||||
owniverse_items[item_id].set_visible(true)
|
||||
else:
|
||||
_new_locked_items.append(_item_id)
|
||||
locked_items = _new_locked_items
|
||||
|
||||
var result: Dictionary = owniverse.presentation.unlocks(owniverse.state, owniverse.catalog, locked_items)
|
||||
for item_id in result.visible:
|
||||
if owniverse_items.has(item_id):
|
||||
owniverse_items[item_id].set_visible(true)
|
||||
locked_items = result.remaining
|
||||
|
||||
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
||||
_set_description(indicator_name, page)
|
||||
@@ -228,7 +214,7 @@ func item_activated(item: OwniverseItem) -> void:
|
||||
|
||||
if item.item_id == "S" or item.item_id == "H":
|
||||
var _value = owniverse.state.amount[item.item_id]
|
||||
_value = 10 * _value if _value > 0 else 1
|
||||
_value = 9 * _value if _value > 0 else 1
|
||||
owniverse.produce_hs(item.item_id, _value)
|
||||
|
||||
|
||||
@@ -504,9 +490,9 @@ func init_enitities() -> Dictionary:
|
||||
var _result := {
|
||||
'locked_items': [],
|
||||
}
|
||||
for entity_id in Global.OwniverseEntities:
|
||||
var entity = OwniverseEntity.new(Global.OwniverseEntities[entity_id])
|
||||
owniverse.entities[entity_id] = entity
|
||||
owniverse.catalog.load_definitions(Global.OwniverseEntities)
|
||||
for entity_id in owniverse.entities:
|
||||
var entity: OwniverseEntity = owniverse.entities[entity_id]
|
||||
|
||||
owniverse.state.stats.total_amount[entity_id] = 0
|
||||
owniverse.state.amount[entity_id] = 0
|
||||
@@ -519,23 +505,8 @@ func init_enitities() -> Dictionary:
|
||||
owniverse.state.stats.total_amount[entity.group_as] = 0
|
||||
|
||||
if entity.unlock != {}:
|
||||
owniverse.locked_entities.append(entity)
|
||||
_result.locked_items.append(entity.id)
|
||||
|
||||
if entity.product_per_year_type != "":
|
||||
owniverse.auto_production.append(entity)
|
||||
|
||||
# groups
|
||||
if entity.group_as != "":
|
||||
if !owniverse.entity_groups.has(entity.group_as):
|
||||
owniverse.entity_groups[entity.group_as] = []
|
||||
owniverse.entity_groups[entity.group_as].append(entity_id)
|
||||
|
||||
if entity.event_group != "":
|
||||
if !owniverse.entity_event_groups.has(entity.event_group):
|
||||
owniverse.entity_event_groups[entity.event_group] = []
|
||||
owniverse.entity_event_groups[entity.event_group].append(entity_id)
|
||||
|
||||
return _result
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
# Проверки поэтапного переноса из Owniverse.U
|
||||
|
||||
## Математика распределений и каталог
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd`
|
||||
|
||||
Проверяются целочисленность и границы распределения, сохранение суммы, разрешение равных остатков, накопление дробей и долгов округления, нормализация весов, размер производственной партии, загрузка каталога и его индексы. Архитектура описана в `../world/owniverse/README.md`. Сохранение игрока не используется.
|
||||
|
||||
## Иконки стадий жизни
|
||||
|
||||
Тест также проверяет модуль `item_presentation.gd`: отсутствие изменений входного состояния и списка закрытых иконок, решения по разблокировке, отложенную и принудительную публикацию количеств.
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd`
|
||||
|
||||
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
|
||||
|
||||
@@ -22,6 +22,7 @@ func run() -> void:
|
||||
manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error]))
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
|
||||
var initial_energy: float = world.state.energy
|
||||
var global: Node = root.get_node("Global")
|
||||
var old_plaintext: bool = global.save_plaintext_copy
|
||||
global.save_plaintext_copy = true
|
||||
@@ -35,9 +36,9 @@ func run() -> void:
|
||||
await process_frame
|
||||
check(not manager.is_saving() and frames > 0, "Frames continue while save is pending")
|
||||
var saved: Dictionary = manager.load_data("snapshot", "test-password")
|
||||
check(saved.state.energy == 1e14 and saved.ui.current_page == 1, "Worker owns a deep snapshot")
|
||||
check(saved.state.energy == initial_energy and saved.ui.current_page == 1, "Worker owns a deep snapshot")
|
||||
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
|
||||
check(plain.state.energy == 1e14, "Diagnostic JSON uses same snapshot")
|
||||
check(plain.state.energy == initial_energy, "Diagnostic JSON uses same snapshot")
|
||||
check(completions.size() == 1 and completions[0][1], "Completion reports successful write")
|
||||
|
||||
completions.clear()
|
||||
|
||||
@@ -33,6 +33,23 @@ func run() -> void:
|
||||
check(counts["C0.0"] == 3.0 and counts.C0 == 3.0 and counts.Planet == 19.0, "Civilization detail does not double the planetary total")
|
||||
check(world.get_item_amounts(true).EON1 == 12.0 and world.get_item_amounts(true).LP == 16.0, "Historical counts retain individual and aggregate values")
|
||||
check(world.state == before, "Presentation does not modify simulation state")
|
||||
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
|
||||
var pending_before: Array = pending.duplicate()
|
||||
var decision: Dictionary = world.presentation.unlocks(world.state, world.catalog, pending)
|
||||
check(decision.remaining == ["EON3"], "Only unmet requirements remain pending")
|
||||
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||
check(decision.visible.has(id), "Unlock projection includes detail and alias: " + id)
|
||||
check(pending == pending_before and world.state == before, "Unlock evaluation leaves inputs untouched")
|
||||
var snapshot: Dictionary = world.items_to_update.duplicate()
|
||||
world.defer_item_updates = true
|
||||
world.state.amount.EON1 += 1.0
|
||||
world.refresh_item_amounts()
|
||||
check(world.item_amounts_dirty and world.items_to_update == snapshot, "Deferred refresh retains pending snapshot")
|
||||
world.refresh_item_amounts(true)
|
||||
check(not world.item_amounts_dirty and world.items_to_update.EON1 == 13.0, "Forced refresh publishes latest amounts")
|
||||
world.state.amount.EON1 -= 1.0
|
||||
world.defer_item_updates = false
|
||||
world.refresh_item_amounts()
|
||||
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||
check(stage.owniverse_items[id].visible, "Unlock both levels: " + id)
|
||||
check(stage.owniverse_items[id].description.text != "", "Populate the icon counter: " + id)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
extends SceneTree
|
||||
|
||||
const Math = preload("res://world/owniverse/quantity_math.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
var Catalog = load("res://world/owniverse/entity_catalog.gd")
|
||||
check(Math.allocate_whole({}, 10.0) == {}, "Empty allocation")
|
||||
check(Math.allocate_whole({"b": 1.0, "a": 1.0}, 1.0) == {"a": 1.0, "b": 0.0}, "Stable lexical tie")
|
||||
for limit in range(30):
|
||||
var requests: Dictionary = {"a": 3.9, "b": 9.1, "c": -2.0, "d": 7.0}
|
||||
var result: Dictionary = Math.allocate_whole(requests, limit)
|
||||
check(Math.sum(result) == minf(limit, 19.0), "Conserve bounded allocation")
|
||||
for id in result:
|
||||
check(result[id] == floorf(result[id]) and result[id] >= 0.0 and result[id] <= floorf(maxf(0.0, requests[id])), "Whole bounded branch")
|
||||
check(Math.normalize_distribution({"a": -1.0, "b": 0.0}) == {}, "Nonpositive weights")
|
||||
check(Math.normalize_distribution({"a": 2.0, "b": 6.0}) == {"a": 0.25, "b": 0.75}, "Normalize weights")
|
||||
check(Math.production_step([0.25, 0.1, 2.0]) == 20.0, "Common whole production step")
|
||||
check(Math.production_step([]) == 1.0, "Free production step")
|
||||
var target: Dictionary = {}
|
||||
var produced: float = 0.0
|
||||
for i in 10:
|
||||
produced += Math.accumulate_whole(target, "remainders", "a", 0.1)
|
||||
check(produced == 1.0, "Fractional production carries")
|
||||
var remainders: Dictionary = {}
|
||||
var totals: Dictionary = {"a": 0.0, "b": 0.0, "c": 0.0}
|
||||
for i in 30:
|
||||
var result: Dictionary = Math.split(remainders, "split", 1.0, {"a": 1.0, "b": 1.0, "c": 1.0})
|
||||
check(Math.sum(result) == 1.0, "Split conserves count")
|
||||
for id in result: totals[id] += result[id]
|
||||
check(totals == {"a": 10.0, "b": 10.0, "c": 10.0}, "Rounding debt distributes over time")
|
||||
produced = 0.0
|
||||
for i in 10: produced += Math.accumulate_rate(remainders, "rate", 1.0, 0.1)
|
||||
check(produced == 1.0, "Rate carries")
|
||||
var catalog = Catalog.new()
|
||||
var rows: Dictionary = root.get_node("Global").OwniverseEntities
|
||||
catalog.load_definitions(rows)
|
||||
check(catalog.entities.size() == rows.size(), "Catalog loads all definitions")
|
||||
for id in catalog.entities:
|
||||
var entity = catalog.entities[id]
|
||||
check(catalog.automatic_producers.has(entity) == (entity.product_per_year_type != ""), "Producer index")
|
||||
check(catalog.unlockable_entities.has(entity) == (not entity.unlock.is_empty()), "Unlock index")
|
||||
if entity.group_as != "":
|
||||
check(catalog.groups[entity.group_as].has(id) and catalog.members(entity.group_as).has(id), "Group index")
|
||||
if entity.event_group != "":
|
||||
check(catalog.event_groups[entity.event_group].has(id), "Event index")
|
||||
var members: Array = catalog.members("Star")
|
||||
members.clear()
|
||||
check(not catalog.members("Star").is_empty(), "Group results are independent")
|
||||
check(Catalog.new().entities.is_empty(), "Catalog instances are independent")
|
||||
for entity in catalog.entities.values(): entity.free()
|
||||
print("Quantity and catalog: %d checks, %d failures" % [checks, failures])
|
||||
quit(1 if failures else 0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvri1enpsjisk
|
||||
+2
-1
@@ -70,7 +70,8 @@ func run() -> void:
|
||||
world.evolute(0.005)
|
||||
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
|
||||
world.speed = 100
|
||||
check(world.speed == 5 and world.state.speed_in_year == 10000.0, "Upper speed limit unchanged")
|
||||
var speed_limit: int = root.get_node("Global").SPEED_LIMIT
|
||||
check(world.speed == speed_limit and world.state.speed_in_year == pow(10.0, speed_limit - 1), "Configured upper speed limit")
|
||||
world.speed = -10
|
||||
check(world.speed == 1 and world.state.speed_in_year == 1.0, "Lower speed limit unchanged")
|
||||
dispose(world)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Модули Owniverse
|
||||
|
||||
Первый этап разделения Owniverse выделяет два модуля:
|
||||
|
||||
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
|
||||
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
|
||||
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Сохранены прежние точности округления: 1e-12 для выпуска и 1e-10 для событий цивилизаций.
|
||||
|
||||
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
|
||||
|
||||
Метод `members(id)` возвращает копию списка. После изменения group_as нужно вызвать `rebuild_group_index()`; добавление или удаление определения автоматически обнаруживается по изменению размера словаря. Полная перезагрузка определений выполняется созданием нового каталога.
|
||||
|
||||
Owniverse сохраняет совместимые свойства entities/entity_groups/entity_event_groups и методы-обёртки. Они используют те же данные каталога, без дублирования. Stage загружает каталог, затем отдельно инициализирует количество, статистику и UI. Эволюция цивилизаций обращается непосредственно к QuantityMath.
|
||||
|
||||
Формулы, порядок обхода определений, формат сохранения и правила симуляции не изменены. Проверки модулей: `tests/test_quantity_catalog.gd`; интеграционные проверки перечислены в `tests/README.md`.
|
||||
|
||||
## Представление количеств и разблокировка иконок
|
||||
|
||||
`item_presentation.gd` получает состояние и каталог, не изменяет симуляцию и не обращается к UI или Global.
|
||||
|
||||
- `amounts(state, catalog, total_produced)` строит текущие или исторические количества. Текущие планеты включают свободные и связанные объекты. Подробные стадии сохраняют собственные значения одновременно с суммой count_as; готовые групповые счётчики повторно не суммируются.
|
||||
- `unlocks(state, catalog, locked)` проверяет условия по историческим количествам и их отображаемым суммам. Возвращает remaining (ещё закрытые сущности) и visible (открываемые сущности и их count_as). Переданный список не изменяется. Уже открытые иконки повторно не блокируются.
|
||||
- `refresh` управляет снимком pending_amounts и флагом dirty, учитывает отложенное обновление и принудительную публикацию. Сам модуль не запускает таймеры и не меняет частоту обновления.
|
||||
|
||||
Owniverse делегирует существующие get_item_amounts/refresh_item_amounts и свойства обновления экземпляру presentation. Stage применяет результат разблокировки к контролам и хранит список закрытых иконок. Снимки и флаги принадлежат отдельному миру и не включаются в сохранение.
|
||||
@@ -1,57 +1,33 @@
|
||||
extends RefCounted
|
||||
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
const State = preload("res://world/owniverse/civilization_state.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const ETHICS = State.ETHICS
|
||||
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
|
||||
|
||||
static func total(pop: Dictionary) -> float:
|
||||
var result: float = 0.0
|
||||
for n in pop.values(): result += n
|
||||
return result
|
||||
return QuantityMath.sum(pop)
|
||||
|
||||
static func add(to: Dictionary, from: Dictionary) -> void:
|
||||
for key in from: to[key] = to.get(key, 0.0) + from[key]
|
||||
|
||||
static func take(world, pop: Dictionary, count: float) -> Dictionary:
|
||||
var taken: Dictionary = world._allocate_whole(pop, count)
|
||||
var taken: Dictionary = QuantityMath.allocate_whole(pop, count)
|
||||
for key in taken: pop[key] -= taken[key]
|
||||
return taken
|
||||
|
||||
static func rate(target: Dictionary, key: String, count: float, probability: float) -> float:
|
||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||
var expected: float = count * maxf(0.0, probability) + target.civilization_remainders.get(key, 0.0)
|
||||
var actual: float = floorf(snappedf(expected, 1e-10))
|
||||
target.civilization_remainders[key] = maxf(0.0, expected - actual)
|
||||
return actual
|
||||
return QuantityMath.accumulate_rate(target.civilization_remainders, key, count, probability)
|
||||
|
||||
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
||||
# Runtime depends on the number of branches, never the number of civilizations.
|
||||
static func split(target: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
var sum_weights: float = total(weights)
|
||||
if count <= 0.0 or sum_weights <= 0.0: return result
|
||||
if count <= 0.0 or QuantityMath.sum(weights) <= 0.0: return {}
|
||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: float = 0.0
|
||||
for id in weights:
|
||||
expected[id] = count * weights[id] / sum_weights + target.civilization_remainders.get(key + ":" + id, 0.0)
|
||||
positive_sum += maxf(0.0, expected[id])
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
for id in weights:
|
||||
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
|
||||
result[id] = floorf(normalized)
|
||||
assigned += result[id]
|
||||
order.append({"id": id, "rest": normalized - result[id]})
|
||||
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||
for entry in order:
|
||||
if assigned >= count: break
|
||||
result[entry.id] += 1.0
|
||||
assigned += 1.0
|
||||
for id in weights:
|
||||
target.civilization_remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
||||
return result
|
||||
return QuantityMath.split(target.civilization_remainders, key, count, weights)
|
||||
|
||||
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary,
|
||||
diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
||||
@@ -132,7 +108,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
var scale: float = maxf(1.0, p + d)
|
||||
var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale)
|
||||
var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale)
|
||||
var bounded: Dictionary = world._allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
|
||||
var bounded: Dictionary = QuantityMath.allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
|
||||
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], minf(1.0, float(row.war) * modifiers.war))
|
||||
if rules[id].colonizes: settlers[ethic] = rate(target, id + ":" + ethic + ":col", triggered[ethic], minf(1.0, float(row.colonization) * modifiers.colonization))
|
||||
successes[ethic] = bounded.pro
|
||||
@@ -143,7 +119,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
if rules[id].fights:
|
||||
for ethic in warriors: warriors[ethic] = minf(warriors[ethic], triggered[ethic])
|
||||
var wars: float = minf(total(warriors), floorf((total(triggered) + total(remaining[id])) / 2.0))
|
||||
warriors = world._allocate_whole(warriors, wars)
|
||||
warriors = QuantityMath.allocate_whole(warriors, wars)
|
||||
for ethic in warriors: triggered[ethic] -= warriors[ethic]
|
||||
var outcomes: Dictionary = split(target, id + ":wars", wars, {"deg": 1.0, "pro25": 2.0, "pro50": 3.0, "pro75": 3.0})
|
||||
var win_expected: float = outcomes.get("pro25", 0.0) * 0.25 + outcomes.get("pro50", 0.0) * 0.5 + outcomes.get("pro75", 0.0) * 0.75
|
||||
@@ -153,7 +129,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
add(triggered, progress(world, target, rules, budget, plan, winners))
|
||||
fall(target, plan, rules, attackers_lost)
|
||||
var losses: float = minf(rate(target, id + ":loss", loss_expected / order, 1.0), total(triggered) + total(remaining[id]))
|
||||
var sides: Dictionary = world._allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
|
||||
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
|
||||
var victims: Dictionary = take(world, triggered, sides.due)
|
||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||
fall(target, plan, rules, victims)
|
||||
@@ -170,7 +146,7 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
var plan: Dictionary = plans[id]
|
||||
var retry: Dictionary = plan.outputs.get(id, {})
|
||||
var amount: float = minf(collateral[id], total(retry) + total(remaining[id]))
|
||||
var sides: Dictionary = world._allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
||||
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
||||
var victims: Dictionary = take(world, retry, sides.due)
|
||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||
fall(target, plan, rules, victims)
|
||||
@@ -223,19 +199,19 @@ static func colonize(world, target: Dictionary, colonies: Array, year: float, ru
|
||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0.0)
|
||||
var requests: Dictionary = {}
|
||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||
var granted: Dictionary = world._allocate_whole(requests, total(stock))
|
||||
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
||||
for i in colonies.size():
|
||||
var colony: Dictionary = colonies[i]
|
||||
var population: Dictionary = world._allocate_whole(colony.population, granted[str(i)])
|
||||
var population: Dictionary = QuantityMath.allocate_whole(colony.population, granted[str(i)])
|
||||
population = drift(target, colony.source + ":colonies", population, rules[colony.source].ethics, false, true)
|
||||
var count: float = total(population)
|
||||
var targets: Dictionary = world._allocate_whole(stock, count)
|
||||
var targets: Dictionary = QuantityMath.allocate_whole(stock, count)
|
||||
for id in targets:
|
||||
var amount: float = targets[id]
|
||||
if amount <= 0.0: continue
|
||||
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
|
||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
|
||||
var taken: Dictionary = world._allocate_whole(slots, amount)
|
||||
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||
world._scale_batches(target, id, 1.0 - amount / stock[id])
|
||||
stock[id] -= amount
|
||||
for owner in taken:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
extends RefCounted
|
||||
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
||||
|
||||
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
||||
@@ -19,7 +21,7 @@ static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
||||
total += current[ethic]
|
||||
if total > count:
|
||||
current = world._allocate_whole(current, count)
|
||||
current = QuantityMath.allocate_whole(current, count)
|
||||
elif total < count:
|
||||
var missing: float = count - total
|
||||
var base: float = floorf(missing / ETHICS.size())
|
||||
@@ -131,7 +133,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
target.amount[id] = supported
|
||||
world._credit(target, previous, excess)
|
||||
world._append_batch(target, previous, excess, year, year)
|
||||
var removed: Dictionary = world._allocate_whole(target.ethics[id], excess)
|
||||
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
|
||||
if not target.ethics.has(previous):
|
||||
target.ethics[previous] = {}
|
||||
for ethic in ETHICS:
|
||||
@@ -143,7 +145,7 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
for star in physical:
|
||||
if world.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
||||
stars[star] = physical[star]
|
||||
var weights: Dictionary = world._normalize_distribution(stars)
|
||||
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
||||
# Worlds are aggregates: a star may host several inhabited planets.
|
||||
var assigned: float = 0.0
|
||||
var owners: Array = weights.keys()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
extends RefCounted
|
||||
## Definitions and their indexes only: no population, batches or UI state.
|
||||
## Definitions are Nodes owned by the caller, as before this extraction.
|
||||
|
||||
var entities: Dictionary[String, OwniverseEntity] = {}
|
||||
var groups: Dictionary[String, Array] = {}
|
||||
var event_groups: Dictionary[String, Array] = {}
|
||||
var automatic_producers: Array = []
|
||||
var unlockable_entities: Array = []
|
||||
var _group_members: Dictionary = {}
|
||||
var _indexed_entity_count: int = -1
|
||||
|
||||
## Populate an empty catalog; replacement uses a new catalog to preserve ownership.
|
||||
func load_definitions(rows: Dictionary) -> void:
|
||||
assert(entities.is_empty(), "Load definitions into an empty catalog")
|
||||
for id in rows:
|
||||
var entity := OwniverseEntity.new(rows[id])
|
||||
entities[id] = entity
|
||||
if not entity.unlock.is_empty(): unlockable_entities.append(entity)
|
||||
if entity.product_per_year_type != "": automatic_producers.append(entity)
|
||||
if entity.group_as != "":
|
||||
if not groups.has(entity.group_as): groups[entity.group_as] = []
|
||||
groups[entity.group_as].append(id)
|
||||
if entity.event_group != "":
|
||||
if not event_groups.has(entity.event_group): event_groups[entity.event_group] = []
|
||||
event_groups[entity.event_group].append(id)
|
||||
rebuild_group_index()
|
||||
|
||||
## Call explicitly after modifying group_as without changing the entity count.
|
||||
func rebuild_group_index() -> void:
|
||||
_group_members.clear()
|
||||
for id in entities:
|
||||
var group: String = entities[id].group_as
|
||||
if not _group_members.has(group): _group_members[group] = []
|
||||
_group_members[group].append(id)
|
||||
_indexed_entity_count = entities.size()
|
||||
|
||||
func _ensure_group_index() -> void:
|
||||
if _indexed_entity_count != entities.size(): rebuild_group_index()
|
||||
|
||||
func is_group(id: String) -> bool:
|
||||
_ensure_group_index()
|
||||
return groups.has(id) or _group_members.has(id)
|
||||
|
||||
func members(id: String) -> Array:
|
||||
if not is_group(id): return [id]
|
||||
return _group_members.get(id, []).duplicate()
|
||||
@@ -0,0 +1 @@
|
||||
uid://6g3m8wmm0ms7
|
||||
@@ -21,7 +21,7 @@ static func matches(world, id: String) -> Array:
|
||||
for key in world.entities:
|
||||
var entity = world.entities[key]
|
||||
if key == id or entity.count_as == id or entity.group_as == id:
|
||||
if not world._is_group(key):
|
||||
if not world.catalog.is_group(key):
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
extends RefCounted
|
||||
## Read-only projections of simulation state. No controls or simulation mutations.
|
||||
|
||||
var pending_amounts: Dictionary[String, float] = {}
|
||||
var deferred_updates: bool = false
|
||||
var dirty: bool = true
|
||||
|
||||
func refresh(state: Dictionary, catalog, force: bool = false) -> void:
|
||||
dirty = true
|
||||
if deferred_updates and not force:
|
||||
return
|
||||
pending_amounts = amounts(state, catalog)
|
||||
dirty = false
|
||||
|
||||
## Aliases are presentation-only; group counters are not counted twice.
|
||||
static func amounts(state: Dictionary, catalog, total_produced: bool = false) -> Dictionary[String, float]:
|
||||
var source: Dictionary = state.stats.total_amount if total_produced else state.amount
|
||||
if not total_produced:
|
||||
source = source.duplicate()
|
||||
var physical: Dictionary = {}
|
||||
for id in catalog.entities:
|
||||
if not catalog.is_group(id):
|
||||
physical[id] = maxf(0.0, state.amount.get(id, 0.0))
|
||||
for bindings: Dictionary in state.binds.values():
|
||||
for id in bindings:
|
||||
if physical.has(id):
|
||||
physical[id] += maxf(0.0, bindings[id])
|
||||
var planets: float = 0.0
|
||||
for id in physical:
|
||||
if catalog.entities[id].group_as == "Planet":
|
||||
source[id] = physical[id]
|
||||
planets += physical[id]
|
||||
source["Planet"] = planets
|
||||
var result: Dictionary[String, float] = {}
|
||||
for id in source:
|
||||
result[id] = 0.0
|
||||
for id in source:
|
||||
# group_as counters are already sums and must not be counted a second time.
|
||||
if catalog.is_group(id):
|
||||
continue
|
||||
var item_id: String = id
|
||||
if catalog.entities.has(id) and catalog.entities[id].count_as != "":
|
||||
item_id = catalog.entities[id].count_as
|
||||
# Detailed icons need their own count as well as the summary alias.
|
||||
# Aggregate only from source, never from these presentation entries.
|
||||
if item_id != id:
|
||||
result[id] = result.get(id, 0.0) + float(source[id])
|
||||
result[item_id] = result.get(item_id, 0.0) + float(source[id])
|
||||
for id in source:
|
||||
if catalog.is_group(id):
|
||||
result[id] = float(source[id])
|
||||
return result
|
||||
|
||||
|
||||
## Evaluate pending icons against historical counts, returning UI decisions only.
|
||||
static func unlocks(state: Dictionary, catalog, locked: Array) -> Dictionary:
|
||||
var remaining: Array = []
|
||||
var visible: Array = []
|
||||
if locked.is_empty():
|
||||
return {"remaining": remaining, "visible": visible}
|
||||
var display_totals: Dictionary = amounts(state, catalog, true)
|
||||
for id in locked:
|
||||
var entity = catalog.entities[id]
|
||||
var eligible: bool = true
|
||||
for requirement in entity.unlock:
|
||||
if maxf(state.stats.total_amount.get(requirement, 0.0), display_totals.get(requirement, 0.0)) < entity.unlock[requirement]:
|
||||
eligible = false
|
||||
break
|
||||
if eligible:
|
||||
for item_id in [id, entity.count_as]:
|
||||
if item_id != "" and not visible.has(item_id):
|
||||
visible.append(item_id)
|
||||
else:
|
||||
remaining.append(id)
|
||||
return {"remaining": remaining, "visible": visible}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdivwab70aqfb
|
||||
+35
-120
@@ -4,22 +4,36 @@ const CivilizationEvolution = preload("res://world/owniverse/civilization_evolut
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
const EntityCatalog = preload("res://world/owniverse/entity_catalog.gd")
|
||||
const ItemPresentation = preload("res://world/owniverse/item_presentation.gd")
|
||||
|
||||
#var state: State = State.new()
|
||||
|
||||
var entities: Dictionary[String, OwniverseEntity] = {}
|
||||
var entity_groups: Dictionary[String, Array] = {}
|
||||
var entity_event_groups: Dictionary[String, Array] = {}
|
||||
var catalog = EntityCatalog.new()
|
||||
# Compatibility accessors keep existing callers and fixtures on the same dictionaries.
|
||||
var entities: Dictionary[String, OwniverseEntity]:
|
||||
get: return catalog.entities
|
||||
var entity_groups: Dictionary[String, Array]:
|
||||
get: return catalog.groups
|
||||
var entity_event_groups: Dictionary[String, Array]:
|
||||
get: return catalog.event_groups
|
||||
|
||||
var _group_members: Dictionary = {}
|
||||
var _indexed_entity_count: int = -1
|
||||
var presentation = ItemPresentation.new()
|
||||
var items_to_update: Dictionary[String, float]:
|
||||
get: return presentation.pending_amounts
|
||||
set(value): presentation.pending_amounts = value
|
||||
var defer_item_updates: bool:
|
||||
get: return presentation.deferred_updates
|
||||
set(value): presentation.deferred_updates = value
|
||||
var item_amounts_dirty: bool:
|
||||
get: return presentation.dirty
|
||||
set(value): presentation.dirty = value
|
||||
|
||||
var items_to_update: Dictionary[String, float] = {}
|
||||
var defer_item_updates: bool = false
|
||||
var item_amounts_dirty: bool = true
|
||||
|
||||
var auto_production: Array = []
|
||||
var locked_entities: Array = []
|
||||
var auto_production: Array:
|
||||
get: return catalog.automatic_producers
|
||||
var locked_entities: Array:
|
||||
get: return catalog.unlockable_entities
|
||||
|
||||
# ------------------------------------
|
||||
|
||||
@@ -123,45 +137,12 @@ func _evolute_to_datetime(datetime: float) -> void:
|
||||
refresh_item_amounts()
|
||||
|
||||
|
||||
## Aggregate only for presentation; never write aliases back into simulation state.
|
||||
func get_item_amounts(total_produced: bool = false) -> Dictionary[String, float]:
|
||||
var source: Dictionary = state.stats.total_amount if total_produced else state.amount
|
||||
if not total_produced:
|
||||
source = source.duplicate()
|
||||
var physical: Dictionary = _physical_amounts(state)
|
||||
var planets: float = 0.0
|
||||
for id in physical:
|
||||
if entities[id].group_as == "Planet":
|
||||
source[id] = physical[id]
|
||||
planets += physical[id]
|
||||
source["Planet"] = planets
|
||||
var result: Dictionary[String, float] = {}
|
||||
for id in source:
|
||||
result[id] = 0.0
|
||||
for id in source:
|
||||
# group_as counters are already sums and must not be counted a second time.
|
||||
if _is_group(id):
|
||||
continue
|
||||
var item_id: String = id
|
||||
if entities.has(id) and entities[id].count_as != "":
|
||||
item_id = entities[id].count_as
|
||||
# Detailed icons need their own count as well as the summary alias.
|
||||
# Aggregate only from source, never from these presentation entries.
|
||||
if item_id != id:
|
||||
result[id] = result.get(id, 0.0) + float(source[id])
|
||||
result[item_id] = result.get(item_id, 0.0) + float(source[id])
|
||||
for id in source:
|
||||
if _is_group(id):
|
||||
result[id] = float(source[id])
|
||||
return result
|
||||
return ItemPresentation.amounts(state, catalog, total_produced)
|
||||
|
||||
|
||||
func refresh_item_amounts(force: bool = false) -> void:
|
||||
item_amounts_dirty = true
|
||||
if defer_item_updates and not force:
|
||||
return
|
||||
items_to_update = get_item_amounts()
|
||||
item_amounts_dirty = false
|
||||
presentation.refresh(state, catalog, force)
|
||||
|
||||
|
||||
func _add_time_delta(delta: float) -> Dictionary:
|
||||
@@ -191,23 +172,11 @@ func modify_world_parameter(key: String, change: float) -> void:
|
||||
## Rebuild after changing group_as or replacing definitions without changing their count.
|
||||
## Adding/removing definitions is detected automatically during initialization.
|
||||
func rebuild_group_index() -> void:
|
||||
_group_members.clear()
|
||||
for id in entities:
|
||||
var group: String = entities[id].group_as
|
||||
if not _group_members.has(group):
|
||||
_group_members[group] = []
|
||||
_group_members[group].append(id)
|
||||
_indexed_entity_count = entities.size()
|
||||
|
||||
|
||||
func _ensure_group_index() -> void:
|
||||
if _indexed_entity_count != entities.size():
|
||||
rebuild_group_index()
|
||||
catalog.rebuild_group_index()
|
||||
|
||||
|
||||
func _is_group(id: String) -> bool:
|
||||
_ensure_group_index()
|
||||
return entity_groups.has(id) or _group_members.has(id)
|
||||
return catalog.is_group(id)
|
||||
|
||||
|
||||
func _physical_amounts(source: Dictionary) -> Dictionary:
|
||||
@@ -223,10 +192,7 @@ func _physical_amounts(source: Dictionary) -> Dictionary:
|
||||
|
||||
|
||||
func _members(id: String) -> Array:
|
||||
if not _is_group(id):
|
||||
return [id]
|
||||
# Callers may modify the returned array without changing the index.
|
||||
return _group_members.get(id, []).duplicate()
|
||||
return catalog.members(id)
|
||||
|
||||
|
||||
func _available(source: Dictionary, id: String) -> float:
|
||||
@@ -607,12 +573,7 @@ func stellar_environment(source: Dictionary) -> Dictionary:
|
||||
|
||||
|
||||
func _accumulate_whole(target: Dictionary, field: String, key: String, value: float) -> float:
|
||||
if not target.has(field):
|
||||
target[field] = {}
|
||||
var expected: float = maxf(0.0, value) + target[field].get(key, 0.0)
|
||||
var whole: float = floorf(snappedf(expected, 1.0e-12))
|
||||
target[field][key] = maxf(0.0, expected - whole)
|
||||
return whole
|
||||
return QuantityMath.accumulate_whole(target, field, key, value)
|
||||
|
||||
|
||||
func _create_planets(target: Dictionary, star_id: String, count: float,
|
||||
@@ -990,27 +951,7 @@ func _expired_years(year: float, start: float, end: float) -> float:
|
||||
|
||||
# Largest-remainder allocation: whole float values, bounded by each request.
|
||||
func _allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
|
||||
var total: float = 0.0
|
||||
for amount in requests.values():
|
||||
total += floorf(maxf(0.0, amount))
|
||||
var available: float = floorf(minf(maxf(0.0, limit), total))
|
||||
var result: Dictionary = {}
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
for id in requests:
|
||||
var expected: float = floorf(maxf(0.0, requests[id])) * available / total if total > 0.0 else 0.0
|
||||
result[id] = floorf(expected)
|
||||
assigned += result[id]
|
||||
order.append({"id": id, "rest": expected - result[id]})
|
||||
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||
for item: Dictionary in order:
|
||||
if assigned >= available:
|
||||
break
|
||||
if result[item.id] < floorf(requests[item.id]):
|
||||
result[item.id] += 1.0
|
||||
assigned += 1.0
|
||||
return result
|
||||
return QuantityMath.allocate_whole(requests, limit)
|
||||
|
||||
|
||||
# The entity currently exposes type/amount fields; life_product can contain
|
||||
@@ -1366,29 +1307,11 @@ func produce_hs(entity_id: String, value: float) -> float:
|
||||
# Find the smallest whole batch whose cost is whole for every resource.
|
||||
# Decimal costs are represented as integer fractions; combine their denominators.
|
||||
func _production_step(entity: OwniverseEntity) -> float:
|
||||
var step: float = 1.0
|
||||
for value in entity.cost.values() + entity.bind.values():
|
||||
var cost: float = maxf(0.0, value)
|
||||
if cost == 0.0:
|
||||
continue
|
||||
var decimal: String = String.num(cost, 12)
|
||||
var point: int = decimal.find(".")
|
||||
var digits: int = decimal.length() - point - 1 if point >= 0 else 0
|
||||
var denominator: float = pow(10.0, digits)
|
||||
var numerator: float = roundf(cost * denominator)
|
||||
if numerator == 0.0:
|
||||
return INF
|
||||
denominator /= _whole_gcd(numerator, denominator)
|
||||
step = step / _whole_gcd(step, denominator) * denominator
|
||||
return step
|
||||
return QuantityMath.production_step(entity.cost.values() + entity.bind.values())
|
||||
|
||||
|
||||
func _whole_gcd(a: float, b: float) -> float:
|
||||
while b > 0.0:
|
||||
var remainder: float = fmod(a, b)
|
||||
a = b
|
||||
b = remainder
|
||||
return a
|
||||
return QuantityMath.whole_gcd(a, b)
|
||||
|
||||
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
|
||||
@@ -1450,15 +1373,7 @@ func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bo
|
||||
## @param values - dictionary containing numeric values
|
||||
## @return dictionary with normalized values, each ranging from 0.0 to 1.0
|
||||
func _normalize_distribution(values: Dictionary) -> Dictionary:
|
||||
var normalized_distribution: Dictionary = {}
|
||||
var distribution_total: float = 0
|
||||
for id in values:
|
||||
if values[id] > 0:
|
||||
distribution_total += values[id]
|
||||
normalized_distribution[id] = values[id]
|
||||
for id in normalized_distribution:
|
||||
normalized_distribution[id] /= distribution_total
|
||||
return normalized_distribution
|
||||
return QuantityMath.normalize_distribution(values)
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
extends RefCounted
|
||||
## Quantity arithmetic without scene tree or global settings.
|
||||
|
||||
static func accumulate_whole(target: Dictionary, field: String, key: String, value: float) -> float:
|
||||
if not target.has(field):
|
||||
target[field] = {}
|
||||
var expected: float = maxf(0.0, value) + target[field].get(key, 0.0)
|
||||
var whole: float = floorf(snappedf(expected, 1.0e-12))
|
||||
target[field][key] = maxf(0.0, expected - whole)
|
||||
return whole
|
||||
|
||||
|
||||
static func allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
|
||||
var total: float = 0.0
|
||||
for amount in requests.values():
|
||||
total += floorf(maxf(0.0, amount))
|
||||
var available: float = floorf(minf(maxf(0.0, limit), total))
|
||||
var result: Dictionary = {}
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
for id in requests:
|
||||
var expected: float = floorf(maxf(0.0, requests[id])) * available / total if total > 0.0 else 0.0
|
||||
result[id] = floorf(expected)
|
||||
assigned += result[id]
|
||||
order.append({"id": id, "rest": expected - result[id]})
|
||||
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||
for item: Dictionary in order:
|
||||
if assigned >= available:
|
||||
break
|
||||
if result[item.id] < floorf(requests[item.id]):
|
||||
result[item.id] += 1.0
|
||||
assigned += 1.0
|
||||
return result
|
||||
|
||||
|
||||
static func production_step(costs: Array) -> float:
|
||||
var step: float = 1.0
|
||||
for value in costs:
|
||||
var cost: float = maxf(0.0, value)
|
||||
if cost == 0.0:
|
||||
continue
|
||||
var decimal: String = String.num(cost, 12)
|
||||
var point: int = decimal.find(".")
|
||||
var digits: int = decimal.length() - point - 1 if point >= 0 else 0
|
||||
var denominator: float = pow(10.0, digits)
|
||||
var numerator: float = roundf(cost * denominator)
|
||||
if numerator == 0.0:
|
||||
return INF
|
||||
denominator /= whole_gcd(numerator, denominator)
|
||||
step = step / whole_gcd(step, denominator) * denominator
|
||||
return step
|
||||
|
||||
|
||||
static func whole_gcd(a: float, b: float) -> float:
|
||||
while b > 0.0:
|
||||
var remainder: float = fmod(a, b)
|
||||
a = b
|
||||
b = remainder
|
||||
return a
|
||||
|
||||
|
||||
static func normalize_distribution(values: Dictionary) -> Dictionary:
|
||||
var normalized_distribution: Dictionary = {}
|
||||
var distribution_total: float = 0
|
||||
for id in values:
|
||||
if values[id] > 0:
|
||||
distribution_total += values[id]
|
||||
normalized_distribution[id] = values[id]
|
||||
for id in normalized_distribution:
|
||||
normalized_distribution[id] /= distribution_total
|
||||
return normalized_distribution
|
||||
|
||||
|
||||
static func sum(pop: Dictionary) -> float:
|
||||
var result: float = 0.0
|
||||
for n in pop.values(): result += n
|
||||
return result
|
||||
|
||||
|
||||
static func split(remainders: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
var sum_weights: float = sum(weights)
|
||||
if count <= 0.0 or sum_weights <= 0.0: return result
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: float = 0.0
|
||||
for id in weights:
|
||||
expected[id] = count * weights[id] / sum_weights + remainders.get(key + ":" + id, 0.0)
|
||||
positive_sum += maxf(0.0, expected[id])
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
for id in weights:
|
||||
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
|
||||
result[id] = floorf(normalized)
|
||||
assigned += result[id]
|
||||
order.append({"id": id, "rest": normalized - result[id]})
|
||||
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
||||
for entry in order:
|
||||
if assigned >= count: break
|
||||
result[entry.id] += 1.0
|
||||
assigned += 1.0
|
||||
for id in weights:
|
||||
remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
||||
return result
|
||||
|
||||
|
||||
static func accumulate_rate(remainders: Dictionary, key: String, count: float, probability: float) -> float:
|
||||
var expected: float = count * maxf(0.0, probability) + remainders.get(key, 0.0)
|
||||
var actual: float = floorf(snappedf(expected, 1e-10))
|
||||
remainders[key] = maxf(0.0, expected - actual)
|
||||
return actual
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfl47j0lvkt2y
|
||||
Reference in New Issue
Block a user