Add exact arithmetic implementation and tests
- Introduced exact arithmetic with support for large integers and fractions in `exact.gd`, `big_integer.gd`, and `exact_value.gd`. - Implemented a comprehensive test suite in `test_exact_math.gd` to validate arithmetic operations against expected results. - Added a README in `math` directory to document the exact arithmetic system and its usage. - Ensured that all arithmetic operations maintain precision without floating-point errors, using a custom representation for large numbers. - Included methods for encoding and decoding exact values for serialization.
This commit is contained in:
+6
-25
@@ -1,4 +1,5 @@
|
||||
extends Node
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const SPEED_LIMIT: int = 6
|
||||
|
||||
@@ -57,37 +58,15 @@ func get_description(item_id: String, items: Array = []) -> String:
|
||||
return render_template(row, variables)
|
||||
|
||||
|
||||
func format_number(value: float, digits: int = 0) -> String:
|
||||
if value == 0: return ""
|
||||
# Keep ordinary quantities readable, grouping thousands with spaces.
|
||||
# Scientific notation is reserved for values whose fixed form would be
|
||||
# unwieldy in the UI.
|
||||
if absf(value) < 1_000_000_000.0:
|
||||
var fixed: String = "%.*f" % [digits, value]
|
||||
var parts: PackedStringArray = fixed.split(".", true)
|
||||
var integer: String = parts[0]
|
||||
var sign_prefix: String = ""
|
||||
if integer.begins_with("-") or integer.begins_with("+"):
|
||||
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_prefix + integer + grouped
|
||||
return grouped + ("." + parts[1] if parts.size() > 1 else "")
|
||||
|
||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
||||
var mantissa = value / pow(10.0, exp_value)
|
||||
|
||||
return "%.*f" % [digits, mantissa] + "E%+d" % exp_value
|
||||
func format_number(value, digits: int = 0) -> String:
|
||||
return E.grouped(value, digits)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
CivilizationRules.clear()
|
||||
for row: Dictionary in _read_from_csv("res://data/civilization_rules.txt").values():
|
||||
if not CivilizationRules.has(row.stage):
|
||||
CivilizationRules[row.stage] = {"home": row.home, "span": float(row.span), "stars": float(row.stars), "ethics": {},
|
||||
CivilizationRules[row.stage] = {"home": row.home, "span": E.of(row.span), "stars": E.of(row.stars), "ethics": {},
|
||||
"colonizes": row.colonizes == "1", "fights": row.fights == "1", "diplomacy": row.diplomacy == "1", "worships": row.worships == "1"}
|
||||
CivilizationRules[row.stage].ethics[row.ethic] = row
|
||||
var stages: Array = CivilizationRules.keys()
|
||||
@@ -107,6 +86,8 @@ func _ready() -> void:
|
||||
Templates["en"] = _read_from_csv("res://data/templates.en.txt")
|
||||
Templates["ru"] = _read_from_csv("res://data/templates.ru.txt")
|
||||
|
||||
#Templates = OwniverseTemplates.get_data()
|
||||
|
||||
|
||||
func _read_from_csv(file_path: String) -> Dictionary:
|
||||
if not FileAccess.file_exists(file_path):
|
||||
|
||||
+128
-129
@@ -1,11 +1,12 @@
|
||||
# SaveManager.gd (добавить в AutoLoad)
|
||||
extends Node
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
|
||||
const SETTING_SECTION := "Settings"
|
||||
const SETTING_SECTION = "Settings"
|
||||
const SAVE_PATH = "user://settings.cfg"
|
||||
|
||||
const FORMAT_VERSION: int = 2
|
||||
const FORMAT_VERSION : int = 3
|
||||
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
|
||||
@@ -21,30 +22,28 @@ var _active_filename: String = ""
|
||||
var _pending_saves: Dictionary = {}
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _save_thread != null and not _save_thread.is_alive():
|
||||
func _process(_delta: Variant) -> void:
|
||||
if (E.ne(_save_thread, null) and (not _save_thread.is_alive())):
|
||||
_finish_save()
|
||||
|
||||
|
||||
func is_saving() -> bool:
|
||||
return _save_thread != null or not _pending_saves.is_empty()
|
||||
return (E.ne(_save_thread, null) or (not _pending_saves.is_empty()))
|
||||
|
||||
|
||||
func _snapshot(filename: String, password: String, data: Dictionary) -> Dictionary:
|
||||
var payload: Dictionary = data.duplicate(true)
|
||||
var payload: Dictionary = E.encode(data)
|
||||
payload.format_version = FORMAT_VERSION
|
||||
payload.app_version = str(ProjectSettings.get_setting("application/config/version", ""))
|
||||
return {"filename": filename, "path": save_directory.path_join(filename + ".adventure"), "password": password, "payload": payload,
|
||||
"plaintext": Global.save_plaintext_copy, "speed_limit": Global.SPEED_LIMIT,
|
||||
"stages": Global.CivilizationRules.keys()}
|
||||
return {"filename": filename, "path": save_directory.path_join(E.add(filename, ".adventure")), "password": password, "payload": payload, "plaintext": Global.save_plaintext_copy, "speed_limit": Global.SPEED_LIMIT, "stages": Global.CivilizationRules.keys()}
|
||||
|
||||
|
||||
## True means queued, not written. Completion is reported by save_completed.
|
||||
func request_save(filename: String, password: String, data: Dictionary) -> bool:
|
||||
if blocked_saves.has(filename):
|
||||
return _fail("Save blocked to preserve an unreadable save: " + filename)
|
||||
return _fail(E.add("Save blocked to preserve an unreadable save: ", filename))
|
||||
_pending_saves[filename] = _snapshot(filename, password, data)
|
||||
if _save_thread == null:
|
||||
if E.eq(_save_thread, null):
|
||||
_start_save()
|
||||
return true
|
||||
|
||||
@@ -57,12 +56,12 @@ func _start_save() -> void:
|
||||
_pending_saves.erase(_active_filename)
|
||||
_save_thread = Thread.new()
|
||||
var error: Error = _save_thread.start(_write_snapshot.bind(job))
|
||||
if error != OK:
|
||||
if E.ne(error, OK):
|
||||
_save_thread = null
|
||||
_fail("Cannot start save worker: " + error_string(error))
|
||||
_fail(E.add("Cannot start save worker: ", error_string(error)))
|
||||
save_completed.emit(_active_filename, false, last_error)
|
||||
_start_save()
|
||||
if not is_saving(): saves_drained.emit()
|
||||
if (not is_saving()): saves_drained.emit()
|
||||
|
||||
|
||||
func _finish_save() -> void:
|
||||
@@ -70,17 +69,17 @@ func _finish_save() -> void:
|
||||
var filename: String = _active_filename
|
||||
_save_thread = null
|
||||
last_error = error
|
||||
if error != "": push_error(error)
|
||||
save_completed.emit(filename, error == "", error)
|
||||
if _save_thread == null: _start_save()
|
||||
if not is_saving(): saves_drained.emit()
|
||||
if E.ne(error, ""): push_error(error)
|
||||
save_completed.emit(filename, E.eq(error, ""), error)
|
||||
if E.eq(_save_thread, null): _start_save()
|
||||
if (not is_saving()): saves_drained.emit()
|
||||
|
||||
|
||||
## Used for application suspension and teardown, when frames may stop arriving.
|
||||
func flush_saves() -> void:
|
||||
while is_saving():
|
||||
if _save_thread == null: _start_save()
|
||||
if _save_thread != null: _finish_save()
|
||||
if E.eq(_save_thread, null): _start_save()
|
||||
if E.ne(_save_thread, null): _finish_save()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
@@ -99,44 +98,44 @@ func save_data(filename: String, password: String, data: Dictionary) -> bool:
|
||||
flush_saves()
|
||||
last_error = ""
|
||||
if blocked_saves.has(filename):
|
||||
return _fail("Save blocked to preserve an unreadable save: " + filename)
|
||||
return _fail(E.add("Save blocked to preserve an unreadable save: ", filename))
|
||||
var error: String = _write_snapshot(_snapshot(filename, password, data))
|
||||
return _fail(error) if error != "" else true
|
||||
return (_fail(error) if E.ne(error, "") else true)
|
||||
|
||||
|
||||
static func _write_snapshot(job: Dictionary) -> String:
|
||||
var payload: Dictionary = job.payload
|
||||
var problem: String = _validate_payload(payload, job.speed_limit, job.stages)
|
||||
if problem != "":
|
||||
return "Invalid save: " + problem
|
||||
if E.ne(problem, ""):
|
||||
return E.add("Invalid save: ", problem)
|
||||
var path: String = job.path
|
||||
var temporary: String = path + ".tmp"
|
||||
var file := FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
||||
if file == null:
|
||||
return "Cannot write encrypted save: " + temporary
|
||||
var temporary: String = E.add(path, ".tmp")
|
||||
var file = FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
||||
if E.eq(file, null):
|
||||
return E.add("Cannot write encrypted save: ", temporary)
|
||||
file.store_var(payload, false)
|
||||
file.close()
|
||||
# Check the actual encrypted file, including finalization, before replacing the old save.
|
||||
var verification := FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, job.password)
|
||||
if verification == null:
|
||||
return "Cannot verify encrypted save: " + temporary
|
||||
var verification = FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, job.password)
|
||||
if E.eq(verification, null):
|
||||
return E.add("Cannot verify encrypted save: ", temporary)
|
||||
var restored: Variant = verification.get_var(false)
|
||||
verification.close()
|
||||
if not restored is Dictionary or restored != payload:
|
||||
return "Encrypted save verification failed: " + temporary
|
||||
if DirAccess.rename_absolute(temporary, path) != OK:
|
||||
return "Cannot replace encrypted save: " + path
|
||||
if ((not (restored is Dictionary)) or E.ne(restored, payload)):
|
||||
return E.add("Encrypted save verification failed: ", temporary)
|
||||
if E.ne(DirAccess.rename_absolute(temporary, path), OK):
|
||||
return E.add("Cannot replace encrypted save: ", path)
|
||||
if job.plaintext:
|
||||
var plain_path: String = path + ".json"
|
||||
file = FileAccess.open(plain_path + ".tmp", FileAccess.WRITE)
|
||||
if file == null:
|
||||
return "Encrypted save written, but cannot write diagnostic copy: " + plain_path
|
||||
file.store_string(JSON.stringify(payload, " ", true, true) + "\n")
|
||||
var plain_path: String = E.add(path, ".json")
|
||||
file = FileAccess.open(E.add(plain_path, ".tmp"), FileAccess.WRITE)
|
||||
if E.eq(file, null):
|
||||
return E.add("Encrypted save written, but cannot write diagnostic copy: ", plain_path)
|
||||
file.store_string(E.add(JSON.stringify(payload, " ", true, true), "\n"))
|
||||
file.flush()
|
||||
var write_error: Error = file.get_error()
|
||||
file.close()
|
||||
if write_error != OK or DirAccess.rename_absolute(plain_path + ".tmp", plain_path) != OK:
|
||||
return "Encrypted save written, but diagnostic copy failed: " + plain_path
|
||||
if (E.ne(write_error, OK) or E.ne(DirAccess.rename_absolute(E.add(plain_path, ".tmp"), plain_path), OK)):
|
||||
return E.add("Encrypted save written, but diagnostic copy failed: ", plain_path)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -144,68 +143,66 @@ func load_data(filename: String, password: String) -> Dictionary:
|
||||
flush_saves()
|
||||
last_error = ""
|
||||
# Plaintext is diagnostic only: never use it as a fallback.
|
||||
var path: String = save_directory.path_join(filename + ".adventure")
|
||||
if not FileAccess.file_exists(path):
|
||||
var path: String = save_directory.path_join(E.add(filename, ".adventure"))
|
||||
if (not FileAccess.file_exists(path)):
|
||||
return {}
|
||||
var file := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
||||
if file == null:
|
||||
var file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
||||
if E.eq(file, null):
|
||||
blocked_saves[filename] = true
|
||||
_fail("Cannot decrypt save: " + path)
|
||||
_fail(E.add("Cannot decrypt save: ", path))
|
||||
return {}
|
||||
var data: Variant = file.get_var(false)
|
||||
file.close()
|
||||
# Unsupported formats are archived, never converted or overwritten.
|
||||
# Returning no data lets the stage start a new, saveable session.
|
||||
if data is Dictionary and data.get("format_version") != FORMAT_VERSION:
|
||||
var archive: String = path + ".incompatible.bak"
|
||||
if ((data is Dictionary) and E.ne(data.get("format_version"), FORMAT_VERSION)):
|
||||
var archive: String = E.add(path, ".incompatible.bak")
|
||||
var index: int = 0
|
||||
while FileAccess.file_exists(archive):
|
||||
index += 1
|
||||
archive = path + ".incompatible.bak." + str(index)
|
||||
index = E.add(index, 1)
|
||||
archive = E.add(E.add(path, ".incompatible.bak."), str(index))
|
||||
var error: Error = DirAccess.rename_absolute(path, archive)
|
||||
if error != OK:
|
||||
if E.ne(error, OK):
|
||||
blocked_saves[filename] = true
|
||||
_fail("Cannot archive incompatible save: " + error_string(error))
|
||||
_fail(E.add("Cannot archive incompatible save: ", error_string(error)))
|
||||
return {}
|
||||
blocked_saves.erase(filename)
|
||||
push_warning("Incompatible save preserved at " + archive + "; starting a new game.")
|
||||
push_warning(E.add(E.add("Incompatible save preserved at ", archive), "; starting a new game."))
|
||||
return {}
|
||||
var problem: String = validate_data(data)
|
||||
if problem != "":
|
||||
if E.ne(problem, ""):
|
||||
blocked_saves[filename] = true
|
||||
_fail("Invalid save " + path + ": " + problem)
|
||||
_fail(E.add(E.add(E.add("Invalid save ", path), ": "), problem))
|
||||
return {}
|
||||
blocked_saves.erase(filename)
|
||||
return data
|
||||
return E.decode(data)
|
||||
|
||||
|
||||
static func _number(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||
if not (value is int or value is float) or not is_finite(float(value)):
|
||||
return false
|
||||
return (not whole or float(value) == floorf(float(value))) and (not nonnegative or value >= 0)
|
||||
return E.numeric(value) and E.finite(value) and (not whole or E.eq(value, E.floor_value(value))) and (not nonnegative or E.ge(value, 0))
|
||||
|
||||
|
||||
static func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||
if not value is Dictionary:
|
||||
if (not (value is Dictionary)):
|
||||
return false
|
||||
for key in value:
|
||||
if not (key is String or key is StringName) or not _number(value[key], whole, nonnegative):
|
||||
if ((not (((key is String) or (key is StringName)))) or (not _number(value[key], whole, nonnegative))):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _plain_tree(value: Variant) -> bool:
|
||||
if value is Dictionary:
|
||||
if (value is Dictionary):
|
||||
for key in value:
|
||||
if not (key is String or key is StringName) or not _plain_tree(value[key]):
|
||||
if ((not (((key is String) or (key is StringName)))) or (not _plain_tree(value[key]))):
|
||||
return false
|
||||
return true
|
||||
if value is Array:
|
||||
if (value is Array):
|
||||
for item in value:
|
||||
if not _plain_tree(item):
|
||||
if (not _plain_tree(item)):
|
||||
return false
|
||||
return true
|
||||
return value == null or value is String or value is StringName or value is bool or _number(value, false, false)
|
||||
return ((((E.eq(value, null) or (value is String)) or (value is StringName)) or (value is bool)) or _number(value, false, false))
|
||||
|
||||
|
||||
## Return a diagnostic path on failure; an empty string means the structure is valid.
|
||||
@@ -214,104 +211,106 @@ func validate_data(data: Variant) -> String:
|
||||
|
||||
|
||||
static func _validate_payload(data: Variant, speed_limit: int, stages: Array) -> String:
|
||||
if not data is Dictionary or not _plain_tree(data):
|
||||
if not E.valid_encoded(data): return "invalid exact number encoding"
|
||||
data = E.decode(data)
|
||||
if ((not (data is Dictionary)) or (not _plain_tree(data))):
|
||||
return "save must contain plain finite values"
|
||||
if data.get("format_version") != FORMAT_VERSION or not data.get("app_version") is String:
|
||||
if (E.ne(data.get("format_version"), FORMAT_VERSION) or (not (data.get("app_version") is String))):
|
||||
return "unsupported save format"
|
||||
if not data.get("ui") is Dictionary or not data.ui.get("current_page") is int or data.ui.current_page < 0:
|
||||
if (((not (data.get("ui") is Dictionary)) or (not (data.ui.get("current_page") is int))) or E.lt(data.ui.current_page, 0)):
|
||||
return "invalid ui.current_page"
|
||||
if not data.get("state") is Dictionary:
|
||||
if (not (data.get("state") is Dictionary)):
|
||||
return "state must be a Dictionary"
|
||||
var saved: Dictionary = data.state
|
||||
var required := [
|
||||
"version", "time", "speed", "speed_in_year", "energy", "batches", "batch_origins",
|
||||
"binds", "amount", "distribution", "auto_resources", "auto_star_deficits",
|
||||
"production_remainders", "resource_remainders", "evolution_remainders", "life_remainders",
|
||||
"planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders",
|
||||
"civilization_stats", "civilization_homes", "ethics", "stats",
|
||||
]
|
||||
var required = ["version", "time", "speed", "speed_in_year", "energy", "batches", "batch_origins", "binds", "amount", "distribution", "auto_resources", "auto_star_deficits", "production_remainders", "resource_remainders", "evolution_remainders", "life_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders", "civilization_stats", "civilization_homes", "ethics", "stats"]
|
||||
# Optional additions to the current format. Saves predating indicators keep
|
||||
# their quantities and receive neutral defaults from Owniverse on load.
|
||||
for field in saved:
|
||||
if field not in required and field not in ["world_parameters", "trends"]:
|
||||
if (not E.contains(required, field) and not E.contains(["world_parameters", "trends"], field)):
|
||||
return "unsupported state structure"
|
||||
if saved.has("world_parameters"):
|
||||
if not _number_map(saved.world_parameters) or saved.world_parameters.size() != WorldParameters.defaults().size():
|
||||
if ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())):
|
||||
return "invalid state.world_parameters"
|
||||
for key in WorldParameters.defaults():
|
||||
if not saved.world_parameters.has(key) or saved.world_parameters[key] < 1.0:
|
||||
return "invalid world parameter " + key
|
||||
if key in ["Bur", "Exp", "Agg"] and saved.world_parameters[key] > 19.0:
|
||||
return "invalid world scale " + key
|
||||
if ((not saved.world_parameters.has(key)) or E.lt(saved.world_parameters[key], 1)):
|
||||
return E.add("invalid world parameter ", key)
|
||||
if (E.contains(["Bur", "Exp", "Agg"], key) and E.gt(saved.world_parameters[key], 19)):
|
||||
return E.add("invalid world scale ", key)
|
||||
if saved.has("trends"):
|
||||
if not _number_map(saved.trends) or saved.trends.size() != WorldParameters.TRENDS.size():
|
||||
if ((not _number_map(saved.trends)) or E.ne(saved.trends.size(), WorldParameters.TRENDS.size())):
|
||||
return "invalid state.trends"
|
||||
for key in WorldParameters.TRENDS:
|
||||
if not saved.trends.has(key) or saved.trends[key] >= 1.0:
|
||||
return "invalid trend " + key
|
||||
if ((not saved.trends.has(key)) or E.ge(saved.trends[key], 1)):
|
||||
return E.add("invalid trend ", key)
|
||||
for field in required:
|
||||
if not saved.has(field):
|
||||
return "missing state." + field
|
||||
if saved.version != STATE_VERSION:
|
||||
if (not saved.has(field)):
|
||||
return E.add("missing state.", field)
|
||||
if E.ne(saved.version, STATE_VERSION):
|
||||
return "unsupported state.version"
|
||||
if not saved.time is Dictionary or not _number(saved.time.get("datetime")) or not _number(saved.time.get("year")):
|
||||
if (((not (saved.time is Dictionary)) or (not _number(saved.time.get("datetime")))) or (not _number(saved.time.get("year")))):
|
||||
return "invalid state.time"
|
||||
if not is_finite(float(saved.time.datetime) * 10.0) or saved.time.year != SimulationTime.processed_year(saved.time.datetime):
|
||||
if ((not E.finite(E.multiply(E.of(saved.time.datetime), 10))) or E.ne(saved.time.year, SimulationTime.processed_year(saved.time.datetime))):
|
||||
return "invalid processed time"
|
||||
if not _number(saved.speed, true) or saved.speed < 1 or saved.speed > speed_limit:
|
||||
if saved.time.has("ticks"):
|
||||
if not _number(saved.time.ticks, true) or not E.eq(saved.time.ticks, E.multiply(saved.time.datetime, SimulationTime.TICKS_PER_YEAR)):
|
||||
return "invalid time ticks"
|
||||
if saved.time.has("tick_remainder"):
|
||||
if not E.finite(saved.time.tick_remainder) or E.ge(E.absolute(saved.time.tick_remainder), 1):
|
||||
return "invalid fractional tick"
|
||||
if (((not _number(saved.speed, true)) or E.lt(saved.speed, 1)) or E.gt(saved.speed, speed_limit)):
|
||||
return "invalid state.speed"
|
||||
if not _number(saved.speed_in_year) or saved.speed_in_year != pow(10.0, saved.speed - 1):
|
||||
if ((not _number(saved.speed_in_year)) or E.ne(saved.speed_in_year, E.power(10, E.subtract(saved.speed, 1)))):
|
||||
return "invalid state.speed_in_year"
|
||||
if not _number(saved.energy, true) or not _number_map(saved.amount, true):
|
||||
if ((not _number(saved.energy, true)) or (not _number_map(saved.amount, true))):
|
||||
return "invalid state resources"
|
||||
if not saved.stats is Dictionary or not _number_map(saved.stats.get("total_amount"), true):
|
||||
if ((not (saved.stats is Dictionary)) or (not _number_map(saved.stats.get("total_amount"), true))):
|
||||
return "invalid state.stats.total_amount"
|
||||
if saved.amount.keys() != saved.stats.total_amount.keys():
|
||||
if E.ne(saved.amount.keys(), saved.stats.total_amount.keys()):
|
||||
return "amount and total_amount keys differ"
|
||||
if not saved.binds is Dictionary or not saved.batches is Dictionary:
|
||||
if ((not (saved.binds is Dictionary)) or (not (saved.batches is Dictionary))):
|
||||
return "state.binds and state.batches must be Dictionaries"
|
||||
for owner_id in saved.binds:
|
||||
if not saved.amount.has(owner_id) or not _number_map(saved.binds[owner_id], true):
|
||||
return "invalid state.binds." + owner_id
|
||||
if ((not saved.amount.has(owner_id)) or (not _number_map(saved.binds[owner_id], true))):
|
||||
return E.add("invalid state.binds.", owner_id)
|
||||
for id in saved.binds[owner_id]:
|
||||
if not saved.amount.has(id):
|
||||
return "unknown bound resource " + id
|
||||
if (not saved.amount.has(id)):
|
||||
return E.add("unknown bound resource ", id)
|
||||
for id in saved.batches:
|
||||
if not saved.amount.has(id) or not saved.batches[id] is Array:
|
||||
return "invalid state.batches." + id
|
||||
if ((not saved.amount.has(id)) or (not (saved.batches[id] is Array))):
|
||||
return E.add("invalid state.batches.", id)
|
||||
for batch in saved.batches[id]:
|
||||
if not batch is Dictionary or not _number(batch.get("amount"), true) or not _number(batch.get("start_year")) or not _number(batch.get("end_year")):
|
||||
return "invalid batch in " + id
|
||||
if batch.end_year < batch.start_year:
|
||||
return "reversed batch interval in " + id
|
||||
if batch.has("created_from") != batch.has("created_to"):
|
||||
return "incomplete batch creation interval in " + id
|
||||
if batch.has("created_from") and (not _number(batch.created_from) or not _number(batch.created_to) or batch.created_to <= batch.created_from or batch.start_year < batch.created_from):
|
||||
return "invalid batch creation interval in " + id
|
||||
if batch.has("processed_until") and (not _number(batch.processed_until) or batch.processed_until < batch.start_year or batch.processed_until > batch.end_year):
|
||||
return "invalid batch.processed_until in " + id
|
||||
if batch.has("withdraw_remainder") and (not _number(batch.withdraw_remainder) or batch.withdraw_remainder > batch.amount):
|
||||
return "invalid batch.withdraw_remainder in " + id
|
||||
if ((((not (batch is Dictionary)) or (not _number(batch.get("amount"), true))) or (not _number(batch.get("start_year")))) or (not _number(batch.get("end_year")))):
|
||||
return E.add("invalid batch in ", id)
|
||||
if E.lt(batch.end_year, batch.start_year):
|
||||
return E.add("reversed batch interval in ", id)
|
||||
if E.ne(batch.has("created_from"), batch.has("created_to")):
|
||||
return E.add("incomplete batch creation interval in ", id)
|
||||
if (batch.has("created_from") and (((((not _number(batch.created_from)) or (not _number(batch.created_to))) or E.le(batch.created_to, batch.created_from)) or E.lt(batch.start_year, batch.created_from)))):
|
||||
return E.add("invalid batch creation interval in ", id)
|
||||
if (batch.has("processed_until") and ((((not _number(batch.processed_until)) or E.lt(batch.processed_until, batch.start_year)) or E.gt(batch.processed_until, batch.end_year)))):
|
||||
return E.add("invalid batch.processed_until in ", id)
|
||||
if (batch.has("withdraw_remainder") and (((not _number(batch.withdraw_remainder)) or E.gt(batch.withdraw_remainder, batch.amount)))):
|
||||
return E.add("invalid batch.withdraw_remainder in ", id)
|
||||
for field in ["batch_origins", "distribution", "production_remainders", "evolution_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "life_remainders", "civilization_remainders", "civilization_stats"]:
|
||||
if not _number_map(saved[field], false, false):
|
||||
return "invalid state." + field
|
||||
if not _number_map(saved.auto_resources, true) or not _number_map(saved.auto_star_deficits, false, false):
|
||||
if (not _number_map(saved[field], false, false)):
|
||||
return E.add("invalid state.", field)
|
||||
if ((not _number_map(saved.auto_resources, true)) or (not _number_map(saved.auto_star_deficits, false, false))):
|
||||
return "invalid automatic production state"
|
||||
if not saved.resource_remainders is Dictionary or saved.resource_remainders.keys().size() != 2 or not saved.resource_remainders.has("basic") or not saved.resource_remainders.has("automatic"):
|
||||
if ((((not (saved.resource_remainders is Dictionary)) or E.ne(saved.resource_remainders.keys().size(), 2)) or (not saved.resource_remainders.has("basic"))) or (not saved.resource_remainders.has("automatic"))):
|
||||
return "invalid state.resource_remainders"
|
||||
for channel in ["basic", "automatic"]:
|
||||
if not _number_map(saved.resource_remainders[channel], false, false):
|
||||
return "invalid state.resource_remainders." + channel
|
||||
if not saved.ethics is Dictionary or not saved.civilization_homes is Dictionary:
|
||||
if (not _number_map(saved.resource_remainders[channel], false, false)):
|
||||
return E.add("invalid state.resource_remainders.", channel)
|
||||
if ((not (saved.ethics is Dictionary)) or (not (saved.civilization_homes is Dictionary))):
|
||||
return "invalid civilization state"
|
||||
for id in saved.ethics:
|
||||
if id not in stages or not _number_map(saved.ethics[id], true):
|
||||
return "invalid state.ethics." + str(id)
|
||||
if (not E.contains(stages, id) or (not _number_map(saved.ethics[id], true))):
|
||||
return E.add("invalid state.ethics.", str(id))
|
||||
for ethic in saved.ethics[id]:
|
||||
if ethic not in ["m", "l", "w", "d", "c", "b"]:
|
||||
return "invalid ethic " + str(ethic)
|
||||
if not E.contains(["m", "l", "w", "d", "c", "b"], ethic):
|
||||
return E.add("invalid ethic ", str(ethic))
|
||||
for id in saved.civilization_homes:
|
||||
if not _number_map(saved.civilization_homes[id], true):
|
||||
if (not _number_map(saved.civilization_homes[id], true)):
|
||||
return "invalid civilization homes"
|
||||
return ""
|
||||
|
||||
@@ -330,7 +329,7 @@ func save_settings():
|
||||
func load_settings():
|
||||
var config = ConfigFile.new()
|
||||
var err = config.load(SAVE_PATH)
|
||||
if err != OK:
|
||||
if E.ne(err, OK):
|
||||
return false
|
||||
for item in Global.Settings:
|
||||
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
class_name CSVLoader
|
||||
|
||||
|
||||
static func load(csv_text: String) -> Array[Dictionary]:
|
||||
var rows: Array[Dictionary] = []
|
||||
|
||||
var lines := csv_text.strip_edges().split("\n")
|
||||
|
||||
if lines.size() < 2:
|
||||
return rows
|
||||
|
||||
var headers := lines[0].split(",")
|
||||
|
||||
for i in range(1, lines.size()):
|
||||
var line := lines[i].strip_edges()
|
||||
|
||||
if line.is_empty():
|
||||
continue
|
||||
|
||||
var values := line.split(",")
|
||||
|
||||
if values.size() != headers.size():
|
||||
push_warning(
|
||||
"CSV row %d has %d columns, expected %d"
|
||||
% [i + 1, values.size(), headers.size()]
|
||||
)
|
||||
continue
|
||||
|
||||
var row: Dictionary = {}
|
||||
|
||||
for j in range(headers.size()):
|
||||
row[headers[j].strip_edges()] = values[j].strip_edges()
|
||||
|
||||
rows.append(row)
|
||||
print(rows)
|
||||
return rows
|
||||
@@ -0,0 +1 @@
|
||||
uid://bfe1fyutcw0cx
|
||||
+2389
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://cunl2sqmcj1ff
|
||||
@@ -1,5 +1,11 @@
|
||||
# Эволюция звёзд Owniverse.GI
|
||||
|
||||
Числа симуляции переведены на целочисленное хранение с большими разрядами и
|
||||
рациональную арифметику. Количества до `10^27` сохраняют единицы; время хранится
|
||||
в тиках по `10^-9` года. Границы точности коэффициентов, ограничение накопленных
|
||||
дробей и несовместимый со старым формат сохранения 3 описаны в
|
||||
[документации математики](world/owniverse/math/README.md).
|
||||
|
||||
Механики перенесены из Owniverse.U с согласованными изменениями GI: постепенные партии, детерминированные дробные остатки, уязвимость цивилизаций до C1.2 и передача выживших планет преемнику. Баланс таблицы GI сохраняется.
|
||||
|
||||
## Порядок расчёта
|
||||
|
||||
+10
-9
@@ -1,4 +1,5 @@
|
||||
extends Control
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
||||
|
||||
@@ -96,11 +97,11 @@ var owniverse_items: Dictionary[String, OwniverseItem] = {}
|
||||
var locked_items: Array = []
|
||||
|
||||
|
||||
func set_datetime(value: float):
|
||||
func set_datetime(value):
|
||||
# Simulation time advances in tenths of a year; displaying five decimal
|
||||
# places only adds zeroes and suggests precision the simulation does not
|
||||
# have.
|
||||
date_time.text = Global.format_number(value, 1) if value != 0.0 else "0.0"
|
||||
date_time.text = Global.format_number(value, 1) if not E.eq(value, 0) else "0.0"
|
||||
|
||||
#region Item manipulations: update and visibility
|
||||
|
||||
@@ -156,8 +157,8 @@ func _update_descriptions(delta: float) -> void:
|
||||
return
|
||||
for page in description_targets:
|
||||
var id: String = description_targets[page].id
|
||||
var total: float = ItemDescription.quantities(owniverse, id).total
|
||||
description_rates[id] = (total - description_samples[id]) / description_elapsed if description_samples.has(id) else 0.0
|
||||
var total = ItemDescription.quantities(owniverse, id).total
|
||||
description_rates[id] = E.divide(E.subtract(total, description_samples[id]), E.of(description_elapsed)) if description_samples.has(id) else 0.0
|
||||
description_samples[id] = total
|
||||
description_elapsed = 0.0
|
||||
_refresh_descriptions()
|
||||
@@ -214,7 +215,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 = 9 * _value if _value > 0 else 1
|
||||
_value = E.multiply(9, _value) if E.gt(_value, 0) else 1
|
||||
owniverse.production.produce_hs(item.item_id, _value)
|
||||
|
||||
|
||||
@@ -284,20 +285,20 @@ func _set_active_page_actions() -> void:
|
||||
var amount = owniverse.production.calculate_max_producible(entity)
|
||||
#print(item_id, " : ", amount)
|
||||
if item_id == "SMBH":
|
||||
var preview: Callable = func(requested: float) -> float:
|
||||
var preview: Callable = func(requested):
|
||||
return owniverse.production.calculate_max_producible(entity, requested)
|
||||
page_actions[pa].set_actions(item_id, amount, preview)
|
||||
else:
|
||||
page_actions[pa].set_actions(item_id, amount)
|
||||
|
||||
|
||||
func _page_action_pressed(page_id: int, action_amount: float) -> void:
|
||||
func _page_action_pressed(page_id: int, action_amount) -> void:
|
||||
if !g_active_items.has(page_id):
|
||||
return
|
||||
|
||||
var item_id: String = g_active_items[page_id].item_id
|
||||
|
||||
if action_amount > 0:
|
||||
if E.gt(action_amount, 0):
|
||||
owniverse.production.produce(item_id, action_amount)
|
||||
|
||||
|
||||
@@ -496,7 +497,7 @@ func init_enitities() -> Dictionary:
|
||||
|
||||
owniverse.state.stats.total_amount[entity_id] = 0
|
||||
owniverse.state.amount[entity_id] = 0
|
||||
if entity.lifespan > 0:
|
||||
if E.gt(entity.lifespan, 0):
|
||||
owniverse.state.batches[entity.id] = []
|
||||
owniverse.batches.add_entity_to_batch(entity_id, 0, 0)
|
||||
if entity.group_as != "":
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Проверки поэтапного переноса из Owniverse.U
|
||||
|
||||
## Точная арифметика
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||
|
||||
Независимый эталон Python Fraction, границы int64 и 10^27, точная сериализация,
|
||||
рецепты с дробными стоимостями, сохранение сумм, время и 100 циклов с меняющимися
|
||||
долями. Все остальные тесты используют числовое сравнение и сериализацию через
|
||||
`E`, а не сравнение ссылок на большие числа. Границы точности и новый формат
|
||||
сохранения описаны в [документации арифметики](../world/owniverse/math/README.md).
|
||||
|
||||
## Модули симуляции
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_module_lifecycle.gd`
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
+21
-20
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
var failures: int = 0
|
||||
var checks: int = 0
|
||||
@@ -7,41 +8,41 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not value):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
var actions = load("res://ui/actions/actions.tscn").instantiate()
|
||||
root.add_child(actions)
|
||||
for maximum in [0.0, 0.1, 0.99, 1.0, 1.9, 10.0, 100.0, 123.0, 1000.0, 10000.0, 1e9, 1.1e9, 1e150]:
|
||||
for maximum in [0, E.of("0.1"), E.of("0.99"), 1, E.of("1.9"), 10, 100, 123, 1000, 10000, E.of("1e9"), E.of("1.1e9"), E.of("1e150")]:
|
||||
actions.set_actions("test", maximum)
|
||||
var labels: Array = []
|
||||
var values: Array = []
|
||||
for i in 5:
|
||||
var value: float = actions.action_values[i]
|
||||
var value: Variant = actions.action_values[i]
|
||||
var text: String = actions.action_labels[i].text
|
||||
check(value == floorf(value) and value <= floorf(maximum), "Whole affordable request")
|
||||
if value > 0.0:
|
||||
check(not values.has(value) and not labels.has(text), "Unique requests and displayed labels")
|
||||
check((E.eq(value, E.floor_value(value)) and E.le(value, E.floor_value(maximum))), "Whole affordable request")
|
||||
if E.gt(value, 0):
|
||||
check(((not values.has(value)) and (not labels.has(text))), "Unique requests and displayed labels")
|
||||
values.append(value)
|
||||
labels.append(text)
|
||||
else:
|
||||
check(text == "" and actions.action_labels[i].get_parent().get_node("TextureButton").disabled, "Unused button blank and disabled")
|
||||
if maximum >= 1.0: check(values.back() == floorf(maximum), "Maximum retained")
|
||||
actions.set_actions("test", 100.0)
|
||||
check(actions.action_values.values() == [1.0, 10.0, 100.0, 0.0, 0.0], "Exact power of ten not duplicated")
|
||||
actions.set_actions("SMBH", 25.0, func(_request): return 25.0)
|
||||
check(actions.action_values.values() == [25.0, 0.0, 0.0, 0.0, 0.0], "Identical SMBH previews collapse to one button")
|
||||
actions.set_actions("step", 123.0, func(request): return floorf(request / 5.0) * 5.0)
|
||||
check(actions.action_values.values() == [10.0, 100.0, 123.0, 0.0, 0.0], "Unproducible small requests omitted")
|
||||
check(actions.action_labels[2].text == "120", "Recipe preview displayed while original request retained")
|
||||
check((E.eq(text, "") and actions.action_labels[i].get_parent().get_node("TextureButton").disabled), "Unused button blank and disabled")
|
||||
if E.ge(maximum, 1): check(E.eq(values.back(), E.floor_value(maximum)), "Maximum retained")
|
||||
actions.set_actions("test", 100)
|
||||
check(E.eq(actions.action_values.values(), [1, 10, 100, 0, 0]), "Exact power of ten not duplicated")
|
||||
actions.set_actions("SMBH", 25, func ( _request ) : return 25)
|
||||
check(E.eq(actions.action_values.values(), [25, 0, 0, 0, 0]), "Identical SMBH previews collapse to one button")
|
||||
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
|
||||
check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted")
|
||||
check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained")
|
||||
var emitted: Array = []
|
||||
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
|
||||
actions._on_action_button_pressed(2)
|
||||
actions._on_action_button_pressed(4)
|
||||
check(emitted == [123.0], "Only active button emits its matching request")
|
||||
check(E.eq(emitted, [123]), "Only active button emits its matching request")
|
||||
actions.queue_free()
|
||||
print("Action values: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Action values: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+43
-38
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
var failures: int = 0
|
||||
var checks: int = 0
|
||||
@@ -8,89 +9,93 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not value):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
var manager = load("res://autoload/save_manager.gd").new()
|
||||
var directory: String = "/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()]
|
||||
var directory: String = ("/tmp/owniverse-async-%s-%s" % [OS.get_process_id(), Time.get_ticks_usec()])
|
||||
DirAccess.make_dir_recursive_absolute(directory)
|
||||
manager.save_directory = directory
|
||||
root.add_child(manager)
|
||||
manager.save_completed.connect(func ( filename , success , error ) : completions.append([filename, success, error]))
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.state.energy = E.subtract(world.state.energy, 1)
|
||||
world.state.time = preload("res://world/owniverse/simulation_time.gd").advance({"datetime": E.of("1e27")}, 0.000000001, 1)
|
||||
var data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
|
||||
var initial_energy: float = world.state.energy
|
||||
var initial_energy: Variant = world.state.energy
|
||||
var global: Node = root.get_node("Global")
|
||||
var old_plaintext: bool = global.save_plaintext_copy
|
||||
global.save_plaintext_copy = true
|
||||
check(manager.request_save("snapshot", "test-password", data), "Snapshot queued")
|
||||
data.state.energy = 99.0
|
||||
data.state.energy = 99
|
||||
data.ui.current_page = 2
|
||||
var frames: int = 0
|
||||
var deadline: int = Time.get_ticks_msec() + 10000
|
||||
while manager.is_saving() and Time.get_ticks_msec() < deadline:
|
||||
frames += 1
|
||||
var deadline: int = E.add(Time.get_ticks_msec(), 10000)
|
||||
while (manager.is_saving() and E.lt(Time.get_ticks_msec(), deadline)):
|
||||
frames = E.add(frames, 1)
|
||||
await process_frame
|
||||
check(not manager.is_saving() and frames > 0, "Frames continue while save is pending")
|
||||
check(((not manager.is_saving()) and E.gt(frames, 0)), "Frames continue while save is pending")
|
||||
var saved: Dictionary = manager.load_data("snapshot", "test-password")
|
||||
check(saved.state.energy == initial_energy and saved.ui.current_page == 1, "Worker owns a deep snapshot")
|
||||
check((E.eq(saved.state.energy, initial_energy) and E.eq(saved.ui.current_page, 1)), "Worker owns a deep snapshot")
|
||||
check(E.eq(saved.state.time, world.state.time), "Encrypted save preserves exact ticks at huge age")
|
||||
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
|
||||
check(plain.state.energy == initial_energy, "Diagnostic JSON uses same snapshot")
|
||||
check(completions.size() == 1 and completions[0][1], "Completion reports successful write")
|
||||
check(E.eq(E.decode(plain.state.energy), initial_energy), "Diagnostic JSON uses same snapshot")
|
||||
check(E.eq(E.decode(plain.state.time), saved.state.time), "Diagnostic JSON preserves fractional time")
|
||||
check((E.eq(completions.size(), 1) and completions[0][1]), "Completion reports successful write")
|
||||
|
||||
completions.clear()
|
||||
data.state.energy = 1.0
|
||||
data.state.energy = 1
|
||||
manager.request_save("latest", "test-password", data)
|
||||
data.state.energy = 2.0
|
||||
data.state.energy = 2
|
||||
manager.request_save("latest", "test-password", data)
|
||||
data.state.energy = 3.0
|
||||
data.state.energy = 3
|
||||
manager.request_save("latest", "test-password", data)
|
||||
data.state.energy = 4.0
|
||||
data.state.energy = 4
|
||||
manager.flush_saves()
|
||||
check(completions.size() == 2, "Repeated requests coalesce behind active write")
|
||||
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Last queued snapshot wins")
|
||||
check(E.eq(completions.size(), 2), "Repeated requests coalesce behind active write")
|
||||
check(E.eq(manager.load_data("latest", "test-password").state.energy, 3), "Last queued snapshot wins")
|
||||
|
||||
var incompatible_path: String = directory.path_join("incompatible.adventure")
|
||||
var incompatible = FileAccess.open_encrypted_with_pass(incompatible_path, FileAccess.WRITE, "test-password")
|
||||
incompatible.store_var({"format_version": -1, "state": {"unknown": true}}, false)
|
||||
incompatible.store_var({"format_version": E.neg(1), "state": {"unknown": true}}, false)
|
||||
incompatible.close()
|
||||
var original: PackedByteArray = FileAccess.get_file_as_bytes(incompatible_path)
|
||||
var previous_archive = FileAccess.open(incompatible_path + ".incompatible.bak", FileAccess.WRITE)
|
||||
var previous_archive = FileAccess.open(E.add(incompatible_path, ".incompatible.bak"), FileAccess.WRITE)
|
||||
previous_archive.store_string("keep previous backup")
|
||||
previous_archive.close()
|
||||
manager.blocked_saves.incompatible = true
|
||||
check(manager.load_data("incompatible", "test-password").is_empty() and manager.last_error == "", "Unsupported format permits a fresh session")
|
||||
check(not manager.blocked_saves.has("incompatible") and not FileAccess.file_exists(incompatible_path), "Incompatible file no longer blocks saving")
|
||||
check(FileAccess.get_file_as_bytes(incompatible_path + ".incompatible.bak.1") == original, "Unsupported save archived without losing bytes")
|
||||
check(FileAccess.get_file_as_string(incompatible_path + ".incompatible.bak") == "keep previous backup", "Earlier archive preserved")
|
||||
check((manager.load_data("incompatible", "test-password").is_empty() and E.eq(manager.last_error, "")), "Unsupported format permits a fresh session")
|
||||
check(((not manager.blocked_saves.has("incompatible")) and (not FileAccess.file_exists(incompatible_path))), "Incompatible file no longer blocks saving")
|
||||
check(E.eq(FileAccess.get_file_as_bytes(E.add(incompatible_path, ".incompatible.bak.1")), original), "Unsupported save archived without losing bytes")
|
||||
check(E.eq(FileAccess.get_file_as_string(E.add(incompatible_path, ".incompatible.bak")), "keep previous backup"), "Earlier archive preserved")
|
||||
check(manager.request_save("incompatible", "test-password", data), "New session can queue a save")
|
||||
manager.flush_saves()
|
||||
check(manager.load_data("incompatible", "test-password").state == data.state, "New format saves and reloads after incompatible file")
|
||||
check(E.eq(manager.load_data("incompatible", "test-password").state, data.state), "New format saves and reloads after incompatible file")
|
||||
|
||||
# Expected failures below must leave the last good encrypted file intact.
|
||||
data.state.energy = -1.0
|
||||
data.state.energy = E.neg(1)
|
||||
manager.request_save("latest", "test-password", data)
|
||||
manager.flush_saves()
|
||||
check(not completions.back()[1] and manager.last_error.contains("Invalid save"), "Worker validation error reaches main thread")
|
||||
check(manager.load_data("latest", "test-password").state.energy == 3.0, "Invalid snapshot preserves previous save")
|
||||
check(((not completions.back()[1]) and manager.last_error.contains("Invalid save")), "Worker validation error reaches main thread")
|
||||
check(E.eq(manager.load_data("latest", "test-password").state.energy, 3), "Invalid snapshot preserves previous save")
|
||||
manager.blocked_saves.latest = true
|
||||
check(not manager.request_save("latest", "test-password", data), "Unreadable save protection also applies to queue")
|
||||
check((not manager.request_save("latest", "test-password", data)), "Unreadable save protection also applies to queue")
|
||||
# A valid snapshot exercises an actual I/O failure, not validation.
|
||||
data.state.energy = 5.0
|
||||
data.state.energy = 5
|
||||
manager.request_save("missing/failure", "test-password", data)
|
||||
manager.flush_saves()
|
||||
check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue")
|
||||
|
||||
manager.request_save("shutdown", "test-password", data)
|
||||
data.state.energy = 6.0
|
||||
data.state.energy = 6
|
||||
manager.request_save("shutdown", "test-password", data)
|
||||
manager.free()
|
||||
var reader = load("res://autoload/save_manager.gd").new()
|
||||
reader.save_directory = directory
|
||||
check(reader.load_data("shutdown", "test-password").state.energy == 6.0, "Teardown drains final pending snapshot")
|
||||
check(E.eq(reader.load_data("shutdown", "test-password").state.energy, 6), "Teardown drains final pending snapshot")
|
||||
reader.free()
|
||||
world.free()
|
||||
# Exercise the stage entry point too: manager tests alone do not catch a
|
||||
@@ -100,12 +105,12 @@ func run() -> void:
|
||||
shared_manager.save_directory = directory
|
||||
var stage = load("res://stage/stage.gd").new()
|
||||
stage.pending_page = 2
|
||||
stage.owniverse.state.energy = 42.0
|
||||
stage.owniverse.state.energy = 42
|
||||
stage.save_data()
|
||||
check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk")
|
||||
stage.owniverse.state.energy = 43.0
|
||||
stage.owniverse.state.energy = 43
|
||||
shared_manager.flush_saves()
|
||||
check(shared_manager.load_data("owniverse", "qwerty").state.energy == 42.0, "Stage save writes an independent snapshot")
|
||||
check(E.eq(shared_manager.load_data("owniverse", "qwerty").state.energy, 42), "Stage save writes an independent snapshot")
|
||||
stage.owniverse.free()
|
||||
stage.free()
|
||||
shared_manager.save_directory = original_directory
|
||||
@@ -113,5 +118,5 @@ func run() -> void:
|
||||
for filename in DirAccess.get_files_at(directory):
|
||||
DirAccess.remove_absolute(directory.path_join(filename))
|
||||
DirAccess.remove_absolute(directory)
|
||||
print("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -37,21 +38,21 @@ func dispose(world) -> void:
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
world.state.batch_origins.LGRB = 10357034.80000000074505806
|
||||
var boundary: float = 41215289.79999999701976776
|
||||
for interval in [[boundary - 2.5, boundary + 2.25], [boundary, boundary + 2.0]]:
|
||||
world.state.batch_origins.LGRB = E.of("10357034.80000000074505806")
|
||||
var boundary: Variant = E.of("41215289.79999999701976776")
|
||||
for interval in [[E.subtract(boundary, E.of("2.5")), E.add(boundary, E.of("2.25"))], [boundary, E.add(boundary, 2)]]:
|
||||
world.state.batches.LGRB = []
|
||||
world.batches.append_batch(world.state, "LGRB", 100.0, interval[0], interval[1])
|
||||
var total: float = 0.0
|
||||
var last: float = -INF
|
||||
world.batches.append_batch(world.state, "LGRB", 100, interval[0], interval[1])
|
||||
var total: Variant = 0
|
||||
var last: Variant = E.neg(INF)
|
||||
for batch : Dictionary in world.state.batches.LGRB:
|
||||
total += batch.amount
|
||||
check(batch.created_from > last, "Batch windows advance monotonically")
|
||||
check(batch.created_to > batch.created_from, "Positive window duration")
|
||||
check(batch.amount >= 0.0, "No negative batch amounts")
|
||||
total = E.add(total, batch.amount)
|
||||
check(E.gt(batch.created_from, last), "Batch windows advance monotonically")
|
||||
check(E.gt(batch.created_to, batch.created_from), "Positive window duration")
|
||||
check(E.ge(batch.amount, 0), "No negative batch amounts")
|
||||
last = batch.created_from
|
||||
check(total == 100.0, "Boundary correction preserves all objects")
|
||||
check(world.state.batches.LGRB.size() <= 7, "Finite number of windows")
|
||||
check(E.eq(total, 100), "Boundary correction preserves all objects")
|
||||
check(E.le(world.state.batches.LGRB.size(), 7), "Finite number of windows")
|
||||
dispose(world)
|
||||
print("Batch boundaries: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Batch boundaries: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -35,130 +36,130 @@ func dispose(world) -> void:
|
||||
entity.free()
|
||||
world.free()
|
||||
|
||||
func seed_population(world, id: String, count: float, time: float = 1.0) -> void:
|
||||
world.state.amount.N = 0.0
|
||||
func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> void:
|
||||
world.state.amount.N = 0
|
||||
world.state.amount[id] = count
|
||||
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
||||
world.evolution.initialize_life_batches()
|
||||
|
||||
func sum_ethics(world, id: String) -> float:
|
||||
var result: float = 0.0
|
||||
for n in world.state.ethics.get(id, {}).values(): result += n
|
||||
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(sum_ethics(world, id) == physical.get(id, 0.0), "Ethics equal physical population for " + id)
|
||||
check(E.eq(sum_ethics(world, id), physical.get(id, 0)), E.add("Ethics equal physical population for ", id))
|
||||
for n in world.state.ethics[id].values():
|
||||
if n < 0.0 or n != floorf(n): check(false, "Invalid ethic amount")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Civilization state validates")
|
||||
if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||
|
||||
func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
||||
var world = fixture()
|
||||
seed_population(world, "C0.2", 60000.0)
|
||||
world.evolution.evolve_batches(world.state, 1.0)
|
||||
check(world.state.amount["C0.3"] > 0.0, "C0.2 actually progresses")
|
||||
check(world.state.amount["C0.1"] > 0.0, "C0.2 actually degrades")
|
||||
check(world.state.civilization_stats.get("wars", 0.0) == 0.0, "Table disables early wars")
|
||||
check(world.state.amount["C0.2"] + world.state.amount["C0.3"] + world.state.amount["C0.1"] == 60000.0, "Ordinary evolution conserves population")
|
||||
check(world.state.stats.total_amount["C0.2"] == 0.0, "Survivors do not count as new civilizations")
|
||||
check(world.state.civilization_stats.worship > 0.0, "Worship derived from surviving ethics")
|
||||
seed_population(world, "C0.2", 60000)
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
check(E.gt(world.state.amount["C0.3"], 0), "C0.2 actually progresses")
|
||||
check(E.gt(world.state.amount["C0.1"], 0), "C0.2 actually degrades")
|
||||
check(E.eq(world.state.civilization_stats.get("wars", 0), 0), "Table disables early wars")
|
||||
check(E.eq(E.add(E.add(world.state.amount["C0.2"], world.state.amount["C0.3"]), world.state.amount["C0.1"]), 60000), "Ordinary evolution conserves population")
|
||||
check(E.eq(world.state.stats.total_amount["C0.2"], 0), "Survivors do not count as new civilizations")
|
||||
check(E.gt(world.state.civilization_stats.worship, 0), "Worship derived from surviving ethics")
|
||||
invariants(world)
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world.evolution.evolve_batches(world.state, 2100.0)
|
||||
restored.evolution.evolve_batches(restored.state, 2100.0)
|
||||
check(world.state == restored.state, "Save resumes deterministic ethical evolution")
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolution.evolve_batches(world.state, 2100)
|
||||
restored.evolution.evolve_batches(restored.state, 2100)
|
||||
check(E.eq(world.state, restored.state), "Save resumes deterministic ethical evolution")
|
||||
invariants(world)
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1e8
|
||||
seed_population(world, "C2.0", 60000.0)
|
||||
world.state.amount["C1.9"] = 10000.0
|
||||
world.state.amount.RD = 100000000
|
||||
seed_population(world, "C2.0", 60000)
|
||||
world.state.amount["C1.9"] = 10000
|
||||
world.evolution.initialize_life_batches()
|
||||
world.evolution.evolve_batches(world.state, 1.0)
|
||||
check(world.state.civilization_stats.wars > 0.0, "Wars execute when enabled")
|
||||
check(world.state.civilization_stats.collateral > 0.0, "Wars affect previous-stage populations")
|
||||
check(world.state.amount["C2.1"] > 0.0, "Winners and peaceful progress reach next stage")
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
check(E.gt(world.state.civilization_stats.wars, 0), "Wars execute when enabled")
|
||||
check(E.gt(world.state.civilization_stats.collateral, 0), "Wars affect previous-stage populations")
|
||||
check(E.gt(world.state.amount["C2.1"], 0), "Winners and peaceful progress reach next stage")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"HP": 5.0, "GG": 5.0, "AB": 5.0, "RP": 5.0}
|
||||
seed_population(world, "C1.6", 60000.0)
|
||||
world.state.amount.RD = 1
|
||||
world.state.binds.RD = {"HP": 5, "GG": 5, "AB": 5, "RP": 5}
|
||||
seed_population(world, "C1.6", 60000)
|
||||
for row : Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
||||
world.evolution.evolve_batches(world.state, 1.0)
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
var after: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
check(world.state.civilization_stats.colonies == 20.0, "Colonization limited to real targets")
|
||||
check(after.HP + after.GG + after.AB + after.RP == 0.0, "Colonized objects consumed once")
|
||||
check(world.state.binds.RD.get("C1.4", 0.0) == 20.0, "Colonies retain target host and use two-stages-earlier level")
|
||||
var totals: float = 0.0
|
||||
check(E.eq(world.state.civilization_stats.colonies, 20), "Colonization limited to real targets")
|
||||
check(E.eq(E.add(E.add(E.add(after.HP, after.GG), after.AB), after.RP), 0), "Colonized objects consumed once")
|
||||
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain target host and use two-stages-earlier level")
|
||||
var totals: Variant = 0
|
||||
for id in after:
|
||||
if world.catalog.entities[id].group_as == "Planet": totals += after[id]
|
||||
check(totals == 60020.0, "Colonization replaces planets, does not create extra worlds")
|
||||
if E.eq(world.catalog.entities[id].group_as, "Planet"): totals = E.add(totals, after[id])
|
||||
check(E.eq(totals, 60020), "Colonization replaces planets, does not create extra worlds")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
|
||||
global.CivilizationRules = baseline.duplicate(true)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1e25
|
||||
world.state.amount.BG = 1e25
|
||||
world.state.amount.TWD = 1e25
|
||||
world.state.amount.SNS = 1e25
|
||||
world.state.amount.MGT = 1e25
|
||||
world.state.amount.BH25 = 1e25
|
||||
world.state.amount.SMBH = 1e26
|
||||
world.state.amount.RD = E.of("1e25")
|
||||
world.state.amount.BG = E.of("1e25")
|
||||
world.state.amount.TWD = E.of("1e25")
|
||||
world.state.amount.SNS = E.of("1e25")
|
||||
world.state.amount.MGT = E.of("1e25")
|
||||
world.state.amount.BH25 = E.of("1e25")
|
||||
world.state.amount.SMBH = E.of("1e26")
|
||||
for id in world.catalog.entities:
|
||||
if world.catalog.entities[id].claim != "": world.state.amount[id] = 1e12
|
||||
if E.ne(world.catalog.entities[id].claim, ""): world.state.amount[id] = E.of("1e12")
|
||||
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
|
||||
for id in global.CivilizationRules:
|
||||
if id == "Ct": continue
|
||||
if E.eq(id, "Ct"): continue
|
||||
global.CivilizationRules[id].fights = false
|
||||
global.CivilizationRules[id].colonizes = false
|
||||
global.CivilizationRules[id].diplomacy = false
|
||||
for row : Dictionary in global.CivilizationRules[id].ethics.values():
|
||||
row.progress = "1"
|
||||
row.degradation = "0"
|
||||
world.catalog.entities[id].lifespan = 1.0
|
||||
world.catalog.entities[id].lifespan_delta = 1.0
|
||||
global.CivilizationRules[id].span = 1.0
|
||||
seed_population(world, "C0.2", 6.0)
|
||||
world.evolution.evolve_batches(world.state, 1000.0)
|
||||
check(world.state.amount.Ct == 6.0, "Complete C0.2 -> C4.4 -> Ct chain runs")
|
||||
check(not world.state.batches.has("Ct"), "Transcendence is terminal")
|
||||
world.catalog.entities[id].lifespan = 1
|
||||
world.catalog.entities[id].lifespan_delta = 1
|
||||
global.CivilizationRules[id].span = 1
|
||||
seed_population(world, "C0.2", 6)
|
||||
world.evolution.evolve_batches(world.state, 1000)
|
||||
check(E.eq(world.state.amount.Ct, 6), "Complete C0.2 -> C4.4 -> Ct chain runs")
|
||||
check((not world.state.batches.has("Ct")), "Transcendence is terminal")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
global.CivilizationRules = baseline
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10000.0
|
||||
world.state.amount.SCS = 1.0
|
||||
world.state.amount["C2.4"] = 1.0
|
||||
world.state.amount.RD = 10000
|
||||
world.state.amount.SCS = 1
|
||||
world.state.amount["C2.4"] = 1
|
||||
world.evolution.initialize_life_batches()
|
||||
var span: float = world.state.batches["C2.4"][0].start_year
|
||||
check(is_equal_approx(span, 20000.0 / 3.6), "Structure bonus shortens newly registered lifespan")
|
||||
var span: Variant = world.state.batches["C2.4"][0].start_year
|
||||
check(E.near(span, E.divide(20000, E.of("3.6"))), "Structure bonus shortens newly registered lifespan")
|
||||
dispose(world)
|
||||
var evolution = load("res://world/owniverse/civilization_evolution.gd")
|
||||
var balanced: Dictionary = {"m": 10000.0, "l": 10000.0, "w": 10000.0, "d": 10000.0, "c": 10000.0, "b": 10000.0}
|
||||
var balanced: Dictionary = {"m": 10000, "l": 10000, "w": 10000, "d": 10000, "c": 10000, "b": 10000}
|
||||
var neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
|
||||
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
|
||||
check(neutral == balanced, "Ordinary drift preserves balanced ethics")
|
||||
check(diplomatic.d > diplomatic.w, "Diplomacy uses per-ethic influence")
|
||||
check(evolution.total(diplomatic) == 60000.0, "Diplomacy conserves population")
|
||||
check(E.eq(neutral, balanced), "Ordinary drift preserves balanced ethics")
|
||||
check(E.gt(diplomatic.d, diplomatic.w), "Diplomacy uses per-ethic influence")
|
||||
check(E.eq(evolution.total(diplomatic), 60000), "Diplomacy conserves population")
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1e15
|
||||
world.state.amount.BG = 1e15
|
||||
world.state.amount["C3.0"] = 6.0
|
||||
world.state.amount.RD = E.of("1e15")
|
||||
world.state.amount.BG = E.of("1e15")
|
||||
world.state.amount["C3.0"] = 6
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.amount["C3.0"] == 0.0 and world.state.amount["C2.9"] == 6.0, "Missing mandatory structures degrade existing civilizations")
|
||||
check((E.eq(world.state.amount["C3.0"], 0) and E.eq(world.state.amount["C2.9"], 6)), "Missing mandatory structures degrade existing civilizations")
|
||||
invariants(world)
|
||||
dispose(world)
|
||||
print("Civilization evolution: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Civilization evolution: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -38,97 +39,97 @@ func dispose(world) -> void:
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
var global: Node = root.get_node("Global")
|
||||
check(global.CivilizationRules.size() == 44, "Load all ethical civilization stages")
|
||||
check(E.eq(global.CivilizationRules.size(), 44), "Load all ethical civilization stages")
|
||||
for row : Dictionary in global.CivilizationRules.values():
|
||||
check(row.ethics.size() == 6 or row.span == 0.0, "Every ethical stage has six rules")
|
||||
world.state.amount.RD = 12.0
|
||||
world.state.amount["C0.2"] = 8.0
|
||||
world.state.binds.RD = {"C2.0": 12.0, "C1.9": 3.0}
|
||||
check((E.eq(row.ethics.size(), 6) or E.eq(row.span, 0)), "Every ethical stage has six rules")
|
||||
world.state.amount.RD = 12
|
||||
world.state.amount["C0.2"] = 8
|
||||
world.state.binds.RD = {"C2.0": 12, "C1.9": 3}
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.amount["C2.0"] == 12.0 and world.state.binds.RD["C2.0"] == 0.0, "Old space civilizations detach without loss")
|
||||
check(world.state.binds.RD["C1.9"] == 3.0, "Planetary civilizations retain their host")
|
||||
check(world.state.ethics["C0.2"] == {"m": 2.0, "l": 2.0, "w": 1.0, "d": 1.0, "c": 1.0, "b": 1.0}, "Migration uses equal ethics and deterministic remainder")
|
||||
check(world.state.ethics["C2.0"].values().all(func(n): return n == 2.0), "Bound civilization ethics count physical population")
|
||||
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")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state == before, "Repeated initialization preserves ethics")
|
||||
world.state.ethics["C0.2"] = {"m": 8.0, "l": 0.0, "w": 0.0, "d": 0.0, "c": 0.0, "b": 0.0}
|
||||
check(E.eq(world.state, before), "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(world.state.ethics["C0.2"].m == 8.0, "Existing unequal distribution is retained")
|
||||
world.state.amount["C0.2"] = 3.0
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 8), "Existing unequal distribution is retained")
|
||||
world.state.amount["C0.2"] = 3
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.ethics["C0.2"].m == 3.0, "Losses reduce ethics without inventing population")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Ethics state validates")
|
||||
check(E.eq(world.state.ethics["C0.2"].m, 3), "Losses reduce ethics without inventing population")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
restored.evolution.initialize_life_batches()
|
||||
check(restored.state == world.state, "Save roundtrip preserves ethics and ownership")
|
||||
check(E.eq(restored.state, world.state), "Save roundtrip preserves ethics and ownership")
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10.0
|
||||
world.state.amount.RD = 10
|
||||
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||
row.progress = "1"
|
||||
row.degradation = "0"
|
||||
row.colonization = "0"
|
||||
world.state.binds.RD = {"C1.9": 10.0}
|
||||
world.state.batches["C1.9"] = [{"amount": 10.0, "start_year": 1.0, "end_year": 1.0}]
|
||||
world.evolution.evolve_batches(world.state, 1.0)
|
||||
check(world.state.amount["C2.0"] == 10.0 and world.state.binds.RD.get("C2.0", 0.0) == 0.0, "Space successor does not inherit planetary binding")
|
||||
world.state.binds.RD = {"C1.9": 10}
|
||||
world.state.batches["C1.9"] = [{"amount": 10, "start_year": 1, "end_year": 1}]
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
check((E.eq(world.state.amount["C2.0"], 10) and E.eq(world.state.binds.RD.get("C2.0", 0), 0)), "Space successor does not inherit planetary binding")
|
||||
dispose(world)
|
||||
var capacity = load("res://world/owniverse/civilization_state.gd")
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10.0
|
||||
world.state.amount["C2.0"] = 8.0
|
||||
world.state.amount.RD = 10
|
||||
world.state.amount["C2.0"] = 8
|
||||
var budget: Dictionary = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 2.0, "Growth consumes only remaining stellar capacity")
|
||||
check(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5.0) == 0.0, "A reservation cannot be spent twice")
|
||||
world.state.amount["C2.0"] = 1.0
|
||||
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5), 2), "Growth consumes only remaining stellar capacity")
|
||||
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5), 0), "A reservation cannot be spent twice")
|
||||
world.state.amount["C2.0"] = 1
|
||||
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||
check(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1.0) == 1.0, "A transition reuses the source reservation")
|
||||
check(budget.free.DwStar == 0.0, "C2.1 reserves ten dwarf stars")
|
||||
world.state.amount["C2.0"] = 0.0
|
||||
world.state.binds.SC = {"RD": 10.0}
|
||||
check(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1), 1), "A transition reuses the source reservation")
|
||||
check(E.eq(budget.free.DwStar, 0), "C2.1 reserves ten dwarf stars")
|
||||
world.state.amount["C2.0"] = 0
|
||||
world.state.binds.SC = {"RD": 10}
|
||||
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
||||
check(budget.capacity.DwStar == 20.0, "Stars inside structures are counted once")
|
||||
check(E.eq(budget.capacity.DwStar, 20), "Stars inside structures are counted once")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 12.0
|
||||
world.state.amount["C2.1"] = 2.0
|
||||
world.state.amount["C2.0"] = 5.0
|
||||
world.state.amount.RD = 12
|
||||
world.state.amount["C2.1"] = 2
|
||||
world.state.amount["C2.0"] = 5
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.amount["C2.1"] == 1.0 and world.state.amount["C2.0"] == 2.0, "Shared budget supports advanced civilizations first")
|
||||
check(world.state.binds.RD["C1.9"] == 4.0, "Excess cascades to supported planetary stage")
|
||||
check((E.eq(world.state.amount["C2.1"], 1) and E.eq(world.state.amount["C2.0"], 2)), "Shared budget supports advanced civilizations first")
|
||||
check(E.eq(world.state.binds.RD["C1.9"], 4), "Excess cascades to supported planetary stage")
|
||||
var snapshot: Dictionary = world.state.duplicate(true)
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state == snapshot, "Capacity enforcement is idempotent")
|
||||
world.state.amount.RD = 0.0
|
||||
check(E.eq(world.state, snapshot), "Capacity enforcement is idempotent")
|
||||
world.state.amount.RD = 0
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.amount["C2.1"] == 0.0 and world.state.amount["C2.0"] == 0.0, "Vanished stars force all unsupported space civilizations down")
|
||||
check(world.ledger.physical_amounts(world.state)["C1.9"] == 7.0, "Forced degradation conserves civilizations")
|
||||
check((E.eq(world.state.amount["C2.1"], 0) and E.eq(world.state.amount["C2.0"], 0)), "Vanished stars force all unsupported space civilizations down")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 7), "Forced degradation conserves civilizations")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.LGW = 20.0
|
||||
world.state.amount.SMBH = 2.0e22
|
||||
world.state.amount["C4.2"] = 3.0
|
||||
world.state.amount.LGW = 20
|
||||
world.state.amount.SMBH = E.of("2.0e22")
|
||||
world.state.amount["C4.2"] = 3
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.amount["C4.2"] == 2.0, "SMBH capacity uses stellar mass equivalent")
|
||||
check(world.ledger.physical_amounts(world.state)["C1.9"] == 1.0, "Unsupported civilization degrades through missing habitat families")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Capacity-limited state validates")
|
||||
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
|
||||
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 2.0
|
||||
world.state.amount.RD = 2
|
||||
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||
row.progress = "1"
|
||||
row.degradation = "0"
|
||||
row.colonization = "0"
|
||||
world.state.binds.RD = {"C1.9": 5.0}
|
||||
world.state.batches["C1.9"] = [{"amount": 5.0, "start_year": 1.0, "end_year": 1.0}]
|
||||
world.evolution.evolve_batches(world.state, 1.0)
|
||||
check(world.state.amount["C2.0"] == 2.0 and world.state.binds.RD["C1.9"] == 3.0, "Unfunded transition retains the original planetary civilization")
|
||||
world.state.binds.RD = {"C1.9": 5}
|
||||
world.state.batches["C1.9"] = [{"amount": 5, "start_year": 1, "end_year": 1}]
|
||||
world.evolution.evolve_batches(world.state, 1)
|
||||
check((E.eq(world.state.amount["C2.0"], 2) and E.eq(world.state.binds.RD["C1.9"], 3)), "Unfunded transition retains the original planetary civilization")
|
||||
dispose(world)
|
||||
print("Civilization state: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Civilization state: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+39
-38
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -39,39 +40,39 @@ func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
var description = load("res://world/owniverse/item_description.gd")
|
||||
var world = fixture()
|
||||
world.state.amount.H = 8.0
|
||||
world.state.amount.He = 2.0
|
||||
world.state.amount.H = 8
|
||||
world.state.amount.He = 2
|
||||
for locale in ["en", "ru"]:
|
||||
global.Locale = locale
|
||||
var text: String = description.render(world, global, "RD")
|
||||
check(text.contains("[b]") and not text.contains("%value"), "Localized template resolved: " + locale)
|
||||
check(text.contains("#40cc40"), "Affordable cost green: " + locale)
|
||||
check(text.contains("4.0000E+10"), "Lifespan substituted: " + locale)
|
||||
check((text.contains("[b]") and (not text.contains("%value"))), E.add("Localized template resolved: ", locale))
|
||||
check(text.contains("#40cc40"), E.add("Affordable cost green: ", locale))
|
||||
check(text.contains("4.0000E+10"), E.add("Lifespan substituted: ", locale))
|
||||
text = description.render(world, global, "He")
|
||||
check(text.contains("He: 20.00% / H: 80.00%") and not text.contains("%value"), "Helium metadata and mixture: " + locale)
|
||||
check((text.contains("He: 20.00% / H: 80.00%") and (not text.contains("%value"))), E.add("Helium metadata and mixture: ", locale))
|
||||
text = description.render(world, global, "Shovelin", "Force")
|
||||
check(text.count("]—[/color]") == 4, "Missing force levels use dash: " + locale)
|
||||
check(E.eq(text.count("]—[/color]"), 4), E.add("Missing force levels use dash: ", locale))
|
||||
text = description.render(world, global, "YD")
|
||||
check(text.contains("#ff6060"), "Insufficient cost red: " + locale)
|
||||
check(text.contains("#ff6060"), E.add("Insufficient cost red: ", locale))
|
||||
var reordered: Dictionary = {"text": "%value0% / %value1%", "value0": "lifespan", "value1": "amount"}
|
||||
check(global.render_template(reordered, {"amount": "12", "lifespan": "99"}) == "99 / 12", "Metadata chooses variables, not fixed argument positions")
|
||||
world.state.amount.RD = 2.0
|
||||
world.state.binds.G = {"RD": 3.0, "HP": 7.0}
|
||||
var values: Dictionary = description.variables(world, global, "RD", -2.0)
|
||||
check(values.amount.contains("2") and values.amount.contains("(5)") and values.amount.contains("#888888"), "Free and total counts with gray binding total")
|
||||
check(values.amount.contains("#ff6060") and values.amount.contains("−2.0"), "Real-time decrease red")
|
||||
check(E.eq(global.render_template(reordered, {"amount": "12", "lifespan": "99"}), "99 / 12"), "Metadata chooses variables, not fixed argument positions")
|
||||
world.state.amount.RD = 2
|
||||
world.state.binds.G = {"RD": 3, "HP": 7}
|
||||
var values: Dictionary = description.variables(world, global, "RD", E.neg(2))
|
||||
check(((values.amount.contains("2") and values.amount.contains("(5)")) and values.amount.contains("#888888")), "Free and total counts with gray binding total")
|
||||
check((values.amount.contains("#ff6060") and values.amount.contains("−2.0")), "Real-time decrease red")
|
||||
values = description.variables(world, global, "HP")
|
||||
check(values.amount == "7", "Planet description includes bound planets")
|
||||
world.state.amount.EON1 = 2.0
|
||||
world.state.binds.G.EON2 = 3.0
|
||||
check(E.eq(values.amount, "7"), "Planet description includes bound planets")
|
||||
world.state.amount.EON1 = 2
|
||||
world.state.binds.G.EON2 = 3
|
||||
var saved_state: Dictionary = world.state.duplicate(true)
|
||||
var counters: Dictionary = world.presentation.amounts(world.state, world.catalog)
|
||||
check(counters.HP == 7.0 and counters.LP == 5.0 and counters.Planet == 12.0, "Planet counters include bound objects and aggregate eons")
|
||||
check(counters.RD == 2.0, "Star counters retain available quantity")
|
||||
check(world.state == saved_state, "Reading counters does not mutate simulation")
|
||||
check(world.presentation.amounts(world.state, world.catalog, true).HP == 0.0, "Produced counters remain independent of current population")
|
||||
values = description.variables(world, global, "RD", 2.0)
|
||||
check(values.amount.contains("#40cc40") and values.amount.contains("+2.0"), "Increase green")
|
||||
check(((E.eq(counters.HP, 7) and E.eq(counters.LP, 5)) and E.eq(counters.Planet, 12)), "Planet counters include bound objects and aggregate eons")
|
||||
check(E.eq(counters.RD, 2), "Star counters retain available quantity")
|
||||
check(E.eq(world.state, saved_state), "Reading counters does not mutate simulation")
|
||||
check(E.eq(world.presentation.amounts(world.state, world.catalog, true).HP, 0), "Produced counters remain independent of current population")
|
||||
values = description.variables(world, global, "RD", 2)
|
||||
check((values.amount.contains("#40cc40") and values.amount.contains("+2.0")), "Increase green")
|
||||
var stage_script = load("res://stage/stage.gd")
|
||||
check(stage_script.can_instantiate(), "Stage integration compiles")
|
||||
var stage = stage_script.new()
|
||||
@@ -81,9 +82,9 @@ func run() -> void:
|
||||
stage.g_item_descriptions = [label]
|
||||
stage.description_targets[0] = {"id": "RD", "category": "Entity"}
|
||||
stage._refresh_descriptions()
|
||||
check(label.bbcode_enabled and label.text.contains("Красный карлик"), "Stage displays localized rich text")
|
||||
check((label.bbcode_enabled and label.text.contains("Красный карлик")), "Stage displays localized rich text")
|
||||
stage._update_descriptions(0.25)
|
||||
world.state.amount.RD += 1.0
|
||||
world.state.amount.RD = E.add(world.state.amount.RD, 1)
|
||||
stage._update_descriptions(0.25)
|
||||
check(label.text.contains("+4.0"), "Selected description refreshes with rate per real second")
|
||||
global.Locale = "en"
|
||||
@@ -92,5 +93,5 @@ func run() -> void:
|
||||
label.free()
|
||||
stage.free()
|
||||
dispose(world)
|
||||
print("Descriptions: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Descriptions: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
extends SceneTree
|
||||
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Q = preload("res://world/owniverse/quantity_math.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
func _initialize():
|
||||
call_deferred("run")
|
||||
|
||||
func check(actual, expected: String, label: String):
|
||||
checks += 1
|
||||
if str(actual) != expected:
|
||||
failures += 1
|
||||
push_error(label + ": " + str(actual) + " != " + expected)
|
||||
|
||||
func run():
|
||||
# Expected strings were generated independently with Python fractions.Fraction.
|
||||
var cases = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/exact_oracle.json"))
|
||||
for row in cases:
|
||||
var a = E.of(row[0])
|
||||
var b = E.of(row[1])
|
||||
check(E.add(a, b), row[2], "sum")
|
||||
check(E.subtract(a, b), row[3], "difference")
|
||||
check(E.multiply(a, b), row[4], "product")
|
||||
check(E.divide(a, b), row[5], "quotient")
|
||||
check(E.floor_value(E.divide(a, b)), row[6], "floor")
|
||||
for text in ["9223372036854775807", "-9223372036854775808", "9007199254740993", "1000000000000000000000000001", "-1/10000000000000"]:
|
||||
var value = E.of(text)
|
||||
check(E.decode(JSON.parse_string(JSON.stringify(E.encode(value)))), text, "JSON round trip")
|
||||
check(E.add(E.of("9223372036854775807"), 1), "9223372036854775808", "int64 overflow promoted")
|
||||
check(E.subtract(E.of("-9223372036854775808"), 1), "-9223372036854775809", "int64 underflow promoted")
|
||||
check(Q.production_step([E.of("1e27")]), "1", "large integral recipe")
|
||||
check(Q.production_step([E.of("1e-13")]), "10000000000000", "tiny fractional recipe")
|
||||
var count = E.of("1000000000000000000000000001")
|
||||
var allocation = Q.allocate_whole({"a": count, "b": count, "c": count}, count)
|
||||
check(Q.sum(allocation), str(count), "allocation conserves every unit")
|
||||
check(allocation.a, "333333333333333333333333334", "deterministic remainder")
|
||||
var clock = Clock.advance({"datetime": E.of("1e27")}, 0.000000001, 1)
|
||||
check(E.subtract(clock.datetime, E.of("1e27")), "1/1000000000", "single tick at huge age")
|
||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||
for id in world.catalog.entities:
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
var group: String = world.catalog.entities[id].group_as
|
||||
if group != "":
|
||||
world.state.amount[group] = 0
|
||||
world.state.stats.total_amount[group] = 0
|
||||
world.production.produce_hs("H", 1)
|
||||
check(world.state.energy, "999999999999999999999999999", "spend one energy")
|
||||
var life = world.life.life_products(world.state, world.catalog.entities.EON5, count)
|
||||
check(Q.sum(life), str(count), "life conservation at 1e27")
|
||||
var precise = E.divide(count, E.of("100000000000000000000000000000000000003"))
|
||||
var bounded = E.bounded(precise)
|
||||
check(E.lt(E.absolute(E.subtract(precise, bounded)), E.of("1e-36")), "true", "bounded fractional error")
|
||||
check(E.bounded(count), str(count), "integer counts never quantized")
|
||||
world.state.amount.S = E.of("1e24")
|
||||
world.state.amount.H = E.of("1e24")
|
||||
world.state.amount.RD = 1234567
|
||||
world.state.amount.YD = 765432
|
||||
world.state.amount.STN = 654321
|
||||
world.speed = 6
|
||||
var started: int = Time.get_ticks_msec()
|
||||
for i in 100:
|
||||
world.state.amount.RD = E.add(world.state.amount.RD, i + 1)
|
||||
world.evolute(0.1)
|
||||
check(str(world.state.auto_star_deficits).length() < 500, "true", "fraction storage stays bounded")
|
||||
print("100 changing-composition cycles: %d ms" % (Time.get_ticks_msec() - started))
|
||||
var saves = root.get_node("SaveManager")
|
||||
check(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), "", "long-running state saves")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
print("Exact math: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://wxhre4pgx1lb
|
||||
+20
-19
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -40,19 +41,19 @@ func run() -> void:
|
||||
for id in world.catalog.entities:
|
||||
var expected: Array = []
|
||||
for key in world.catalog.entities:
|
||||
if world.catalog.entities[key].group_as == id:
|
||||
if E.eq(world.catalog.entities[key].group_as, id):
|
||||
expected.append(key)
|
||||
check(world.catalog.is_group(id) == (not expected.is_empty()), "Group classification: " + id)
|
||||
check(world.catalog.members(id) == (expected if not expected.is_empty() else [id]), "Group members: " + id)
|
||||
check(E.eq(world.catalog.is_group(id), ((not expected.is_empty()))), E.add("Group classification: ", id))
|
||||
check(E.eq(world.catalog.members(id), ((expected if (not expected.is_empty()) else [id]))), E.add("Group members: ", id))
|
||||
var members: Array = world.catalog.members("Star")
|
||||
members.clear()
|
||||
check(not world.catalog.members("Star").is_empty(), "Returned members are independent")
|
||||
check((not world.catalog.members("Star").is_empty()), "Returned members are independent")
|
||||
world.catalog.entities.N.group_as = "TestGroup"
|
||||
world.catalog.rebuild_group_index()
|
||||
check(world.catalog.members("TestGroup") == ["N"], "Explicit definition reload")
|
||||
check(E.eq(world.catalog.members("TestGroup"), ["N"]), "Explicit definition reload")
|
||||
world.catalog.entities.N.free()
|
||||
world.catalog.entities.erase("N")
|
||||
check(not world.catalog.is_group("TestGroup"), "Removed definition invalidates index")
|
||||
print("Group index: %d checks, %d failures" % [checks, failures])
|
||||
check((not world.catalog.is_group("TestGroup")), "Removed definition invalidates index")
|
||||
print(("Group index: %d checks, %d failures" % [checks, failures]))
|
||||
dispose(world)
|
||||
quit(1 if failures else 0)
|
||||
quit((1 if failures else 0))
|
||||
|
||||
+65
-64
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -37,87 +38,87 @@ func dispose(world) -> void:
|
||||
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.HP = 40000.0
|
||||
world.state.amount.N = 0
|
||||
world.state.amount.HP = 40000
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.batches.HP.size() == 1, "Old HP receives life cohort")
|
||||
check(world.state.batches.HP[0].start_year == 5e6, "HP uses source span")
|
||||
check(E.eq(world.state.batches.HP.size(), 1), "Old HP receives life cohort")
|
||||
check(E.eq(world.state.batches.HP[0].start_year, 5000000), "HP uses source span")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state == before, "Initialization is idempotent")
|
||||
world.evolution.evolve_batches(world.state, 5e6 + 5e6 / 40.0)
|
||||
check(world.state.amount.EON1 == 10.0, "HP abiogenesis follows source probability")
|
||||
check(world.state.amount.RP == 100.0, "HP degradation produces RP")
|
||||
check(world.state.amount.HP == 39890.0, "Unchanged planets retry, not disappear")
|
||||
check(world.state.stats.total_amount.HP == 0.0, "Retry does not inflate creation statistics")
|
||||
check(world.state.batches.HP.size() > 0, "Survivors get next attempt")
|
||||
check(E.eq(world.state, before), "Initialization is idempotent")
|
||||
world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, 40)))
|
||||
check(E.eq(world.state.amount.EON1, 10), "HP abiogenesis follows source probability")
|
||||
check(E.eq(world.state.amount.RP, 100), "HP degradation produces RP")
|
||||
check(E.eq(world.state.amount.HP, 39890), "Unchanged planets retry, not disappear")
|
||||
check(E.eq(world.state.stats.total_amount.HP, 0), "Retry does not inflate creation statistics")
|
||||
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
check(physical.HP + physical.EON1 + physical.RP == 40000.0, "Life conserves planets")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Life save validates")
|
||||
check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "Life conserves planets")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world.evolution.evolve_batches(world.state, 11e6)
|
||||
restored.evolution.evolve_batches(restored.state, 11e6)
|
||||
check(world.state == restored.state, "Life continuation preserves cohorts and carries")
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolution.evolve_batches(world.state, 11000000)
|
||||
restored.evolution.evolve_batches(restored.state, 11000000)
|
||||
check(E.eq(world.state, restored.state), "Life continuation preserves cohorts and carries")
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"EON5": 40000.0}
|
||||
world.state.amount.RD = 1
|
||||
world.state.binds.RD = {"EON5": 40000}
|
||||
world.evolution.initialize_life_batches()
|
||||
world.evolution.evolve_batches(world.state, 5e5 + 5e5 / 40.0)
|
||||
check(world.state.binds.RD["C0.0"] == 10.0, "Intelligence emerges at the same host star")
|
||||
check(world.state.amount["C0.0"] == 0.0, "Life transition keeps planet bound")
|
||||
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, 40)))
|
||||
check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star")
|
||||
check(E.eq(world.state.amount["C0.0"], 0), "Life transition keeps planet bound")
|
||||
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
var progress: float = 0.0
|
||||
var degraded: float = 0.0
|
||||
var progress: Variant = 0
|
||||
var degraded: Variant = 0
|
||||
var valid_outcomes: bool = true
|
||||
for i in 4001:
|
||||
var products: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1.0)
|
||||
var total: float = 0.0
|
||||
var products: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1)
|
||||
var total: Variant = 0
|
||||
for value in products.values():
|
||||
total += value
|
||||
valid_outcomes = valid_outcomes and value >= 0.0 and value == floorf(value)
|
||||
valid_outcomes = valid_outcomes and total == 1.0
|
||||
progress += products.get("EON1", 0.0)
|
||||
degraded += products.get("RP", 0.0)
|
||||
total = E.add(total, value)
|
||||
valid_outcomes = ((valid_outcomes and E.ge(value, 0)) and E.eq(value, E.floor_value(value)))
|
||||
valid_outcomes = (valid_outcomes and E.eq(total, 1))
|
||||
progress = E.add(progress, products.get("EON1", 0))
|
||||
degraded = E.add(degraded, products.get("RP", 0))
|
||||
check(valid_outcomes, "Every single-planet attempt has whole outcomes and conserves population")
|
||||
check(progress == 1.0 and degraded == 10.0, "Rare outcomes accumulate deterministically")
|
||||
check((E.eq(progress, 1) and E.eq(degraded, 10)), "Rare outcomes accumulate deterministically")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
||||
world.state.binds.RD = {"HP": 2.0}
|
||||
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
|
||||
world.state.binds.RD = {"HP": 2}
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.batches.HP[0].created_from == 123.4, "Old bound worlds begin life at load time")
|
||||
world.stellar.destroy_planets(world.state, "RD", 1.0, 1)
|
||||
world.evolution.evolve_batches(world.state, 1e7)
|
||||
check(world.ledger.physical_amounts(world.state).HP == 0.0 and world.state.binds.RD.RP == 2.0, "Destroyed life does not resurrect from cohort")
|
||||
check(E.eq(world.state.batches.HP[0].created_from, E.of("123.4")), "Old bound worlds begin life at load time")
|
||||
world.stellar.destroy_planets(world.state, "RD", 1, 1)
|
||||
world.evolution.evolve_batches(world.state, 10000000)
|
||||
check((E.eq(world.ledger.physical_amounts(world.state).HP, 0) and E.eq(world.state.binds.RD.RP, 2)), "Destroyed life does not resurrect from cohort")
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
||||
world.state.amount["C0.0"] = 40000.0
|
||||
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
|
||||
world.state.amount["C0.0"] = 40000
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state.batches["C0.0"][0].created_from == 123.4, "Civilization cohorts begin at the current time")
|
||||
check(E.eq(world.state.batches["C0.0"][0].created_from, E.of("123.4")), "Civilization cohorts begin at the current time")
|
||||
before = world.state.duplicate(true)
|
||||
world.evolution.initialize_life_batches()
|
||||
check(world.state == before, "Life batch initialization is idempotent")
|
||||
world.evolution.evolve_batches(world.state, 2173.4)
|
||||
check(world.state.amount["C0.1"] == 700.0 and world.state.amount.EON5 == 1000.0, "C0.0 progresses and degrades by Unity rates")
|
||||
check(world.state.amount["C0.0"] == 38300.0, "C0.0 remainder stays alive")
|
||||
check(E.eq(world.state, before), "Life batch initialization is idempotent")
|
||||
world.evolution.evolve_batches(world.state, E.of("2173.4"))
|
||||
check((E.eq(world.state.amount["C0.1"], 700) and E.eq(world.state.amount.EON5, 1000)), "C0.0 progresses and degrades by Unity rates")
|
||||
check(E.eq(world.state.amount["C0.0"], 38300), "C0.0 remainder stays alive")
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"C0.1": 40000.0}
|
||||
world.state.amount.RD = 1
|
||||
world.state.binds.RD = {"C0.1": 40000}
|
||||
world.evolution.initialize_life_batches()
|
||||
world.evolution.evolve_batches(world.state, 2050.0)
|
||||
check(world.state.binds.RD["C0.2"] == 700.0, "C0.1 reaches ethical civilization without losing host")
|
||||
check(world.state.binds.RD["C0.0"] == 1000.0, "C0.1 degradation follows Unity")
|
||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Extended life save validates")
|
||||
world.evolution.evolve_batches(world.state, 2050)
|
||||
check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host")
|
||||
check(E.eq(world.state.binds.RD["C0.0"], 1000), "C0.1 degradation follows Unity")
|
||||
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||
dispose(world)
|
||||
print("Life: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Life: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+28
-27
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not value):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
@@ -21,48 +22,48 @@ func run() -> void:
|
||||
await process_frame
|
||||
await process_frame
|
||||
var world = stage.owniverse
|
||||
world.state.binds.RD = {"EON1": 10.0, "EON2": 4.0, "C0.0": 3.0}
|
||||
world.state.amount.EON1 = 2.0
|
||||
world.state.stats.total_amount.EON1 = 12.0
|
||||
world.state.stats.total_amount.EON2 = 4.0
|
||||
world.state.stats.total_amount["C0.0"] = 3.0
|
||||
world.state.binds.RD = {"EON1": 10, "EON2": 4, "C0.0": 3}
|
||||
world.state.amount.EON1 = 2
|
||||
world.state.stats.total_amount.EON1 = 12
|
||||
world.state.stats.total_amount.EON2 = 4
|
||||
world.state.stats.total_amount["C0.0"] = 3
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
stage._refresh_universe_ui()
|
||||
var counts: Dictionary = world.presentation.amounts(world.state, world.catalog)
|
||||
check(counts.EON1 == 12.0 and counts.EON2 == 4.0 and counts.LP == 16.0, "Individual eons and their total include bound planets")
|
||||
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.presentation.amounts(world.state, world.catalog, true).EON1 == 12.0 and world.presentation.amounts(world.state, world.catalog, true).LP == 16.0, "Historical counts retain individual and aggregate values")
|
||||
check(world.state == before, "Presentation does not modify simulation state")
|
||||
check(((E.eq(counts.EON1, 12) and E.eq(counts.EON2, 4)) and E.eq(counts.LP, 16)), "Individual eons and their total include bound planets")
|
||||
check(((E.eq(counts["C0.0"], 3) and E.eq(counts.C0, 3)) and E.eq(counts.Planet, 19)), "Civilization detail does not double the planetary total")
|
||||
check((E.eq(world.presentation.amounts(world.state, world.catalog, true).EON1, 12) and E.eq(world.presentation.amounts(world.state, world.catalog, true).LP, 16)), "Historical counts retain individual and aggregate values")
|
||||
check(E.eq(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")
|
||||
check(E.eq(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")
|
||||
check(decision.visible.has(id), E.add("Unlock projection includes detail and alias: ", id))
|
||||
check((E.eq(pending, pending_before) and E.eq(world.state, before)), "Unlock evaluation leaves inputs untouched")
|
||||
var snapshot: Dictionary = world.presentation.pending_amounts.duplicate()
|
||||
world.presentation.deferred_updates = true
|
||||
world.state.amount.EON1 += 1.0
|
||||
world.state.amount.EON1 = E.add(world.state.amount.EON1, 1)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
check(world.presentation.dirty and world.presentation.pending_amounts == snapshot, "Deferred refresh retains pending snapshot")
|
||||
check((world.presentation.dirty and E.eq(world.presentation.pending_amounts, snapshot)), "Deferred refresh retains pending snapshot")
|
||||
world.presentation.refresh(world.state, world.catalog, true)
|
||||
check(not world.presentation.dirty and world.presentation.pending_amounts.EON1 == 13.0, "Forced refresh publishes latest amounts")
|
||||
world.state.amount.EON1 -= 1.0
|
||||
check(((not world.presentation.dirty) and E.eq(world.presentation.pending_amounts.EON1, 13)), "Forced refresh publishes latest amounts")
|
||||
world.state.amount.EON1 = E.subtract(world.state.amount.EON1, 1)
|
||||
world.presentation.deferred_updates = false
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
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)
|
||||
check(not stage.owniverse_items.EON3.visible, "Unreached eon remains locked")
|
||||
check(stage.owniverse_items[id].visible, E.add("Unlock both levels: ", id))
|
||||
check(E.ne(stage.owniverse_items[id].description.text, ""), E.add("Populate the icon counter: ", id))
|
||||
check((not stage.owniverse_items.EON3.visible), "Unreached eon remains locked")
|
||||
stage.item_activated(stage.owniverse_items.LP)
|
||||
check(stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting LP shows its unlocked eons")
|
||||
stage.item_activated(stage.owniverse_items.C0)
|
||||
check(stage.owniverse_items["C0.0"].is_visible_in_tree() and not stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting C0 switches the detail panel")
|
||||
world.state.binds.RD.EON2 = 0.0
|
||||
check((stage.owniverse_items["C0.0"].is_visible_in_tree() and (not stage.owniverse_items.EON1.is_visible_in_tree())), "Selecting C0 switches the detail panel")
|
||||
world.state.binds.RD.EON2 = 0
|
||||
stage._refresh_universe_ui()
|
||||
check(stage.owniverse_items.EON2.visible and stage.owniverse_items.EON2.description.text == "", "Previously reached stage stays unlocked after extinction with no stale count")
|
||||
check((stage.owniverse_items.EON2.visible and E.eq(stage.owniverse_items.EON2.description.text, "")), "Previously reached stage stays unlocked after extinction with no stale count")
|
||||
for entity in world.catalog.entities.values(): entity.free()
|
||||
world.free()
|
||||
scene.free()
|
||||
print("Life icons: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Life icons: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
@@ -19,20 +20,20 @@ func run() -> void:
|
||||
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"]
|
||||
var references: Array = []
|
||||
for name in names:
|
||||
check(first.get(name) != second.get(name), "World has its own " + name)
|
||||
check(first.get(name).world == first, "Module refers to its owner: " + name)
|
||||
check(E.ne(first.get(name), second.get(name)), E.add("World has its own ", name))
|
||||
check(E.eq(first.get(name).world, first), E.add("Module refers to its owner: ", name))
|
||||
references.append(weakref(first.get(name)))
|
||||
first.state.amount.H = 123.0
|
||||
first.presentation.pending_amounts.H = 123.0
|
||||
check(not second.state.amount.has("H") and second.presentation.pending_amounts.is_empty(), "State and pending presentation are isolated")
|
||||
first.state.amount.H = 123
|
||||
first.presentation.pending_amounts.H = 123
|
||||
check(((not second.state.amount.has("H")) and second.presentation.pending_amounts.is_empty()), "State and pending presentation are isolated")
|
||||
first.free()
|
||||
for reference in references:
|
||||
check(reference.get_ref() == null, "Module is released with its world")
|
||||
check(second.evolution.world == second, "Other world remains usable")
|
||||
check(E.eq(reference.get_ref(), null), "Module is released with its world")
|
||||
check(E.eq(second.evolution.world, second), "Other world remains usable")
|
||||
var snapshot: Dictionary = second.state.duplicate(true)
|
||||
second.evolute(NAN)
|
||||
second.evolute(-1.0)
|
||||
check(second.state == snapshot, "Entry point rejects invalid time deltas")
|
||||
second.evolute(E.neg(1))
|
||||
check(E.eq(second.state, snapshot), "Entry point rejects invalid time deltas")
|
||||
second.free()
|
||||
print("Module lifecycle: %d checks, %d failures" % [checks, failures])
|
||||
quit(1 if failures else 0)
|
||||
print(("Module lifecycle: %d checks, %d failures" % [checks, failures]))
|
||||
quit((1 if failures else 0))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -38,21 +39,21 @@ func dispose(world) -> void:
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
world.evolution.initialize_life_batches()
|
||||
world.catalog.entities.N.cost = {"H": 2.0}
|
||||
world.catalog.entities.N.bind = {"S": 3.0}
|
||||
world.state.amount.H = 10.0
|
||||
world.state.amount.S = 0.0
|
||||
world.state.auto_resources = {"H": 10.0}
|
||||
world.state.batches.H = [{"amount": 10.0, "start_year": 1.0, "end_year": 2.0}]
|
||||
world.catalog.entities.N.cost = {"H": 2}
|
||||
world.catalog.entities.N.bind = {"S": 3}
|
||||
world.state.amount.H = 10
|
||||
world.state.amount.S = 0
|
||||
world.state.auto_resources = {"H": 10}
|
||||
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
check(not world.production.try_production(world.state, world.catalog.entities.N, 1.0, true), "Binding failure rejects production")
|
||||
check(world.state == before, "Failure preserves costs, batches and budget")
|
||||
world.state.amount.S = 10.0
|
||||
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true)), "Binding failure rejects production")
|
||||
check(E.eq(world.state, before), "Failure preserves costs, batches and budget")
|
||||
world.state.amount.S = 10
|
||||
var old_batches: Array = world.state.batches.H
|
||||
check(world.production.try_production(world.state, world.catalog.entities.N, 2.0, true), "Funded production succeeds")
|
||||
check(world.state.amount.H == 6.0 and world.state.amount.S == 4.0, "Exact cost and binding")
|
||||
check(world.state.auto_resources.H == 6.0, "Exact automatic budget")
|
||||
check(old_batches[0].amount == 10.0 and world.state.batches.H[0].amount == 6.0, "Consumed batches isolated")
|
||||
print("Transaction: %d checks, %d failures" % [checks, failures])
|
||||
check(world.production.try_production(world.state, world.catalog.entities.N, 2, true), "Funded production succeeds")
|
||||
check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding")
|
||||
check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget")
|
||||
check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated")
|
||||
print(("Transaction: %d checks, %d failures" % [checks, failures]))
|
||||
dispose(world)
|
||||
quit(1 if failures else 0)
|
||||
quit((1 if failures else 0))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const Math = preload("res://world/owniverse/quantity_math.gd")
|
||||
var checks: int = 0
|
||||
@@ -8,56 +9,56 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(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")
|
||||
check(E.eq(Math.allocate_whole({}, 10), {}), "Empty allocation")
|
||||
check(E.eq(Math.allocate_whole({"b": 1, "a": 1}, 1), {"a": 1, "b": 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 requests: Dictionary = {"a": E.of("3.9"), "b": E.of("9.1"), "c": E.neg(2), "d": 7}
|
||||
var result: Dictionary = Math.allocate_whole(requests, limit)
|
||||
check(Math.sum(result) == minf(limit, 19.0), "Conserve bounded allocation")
|
||||
check(E.eq(Math.sum(result), E.minimum(limit, 19)), "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")
|
||||
check(((E.eq(result[id], E.floor_value(result[id])) and E.ge(result[id], 0)) and E.le(result[id], E.floor_value(E.maximum(0, requests[id])))), "Whole bounded branch")
|
||||
check(E.eq(Math.normalize_distribution({"a": E.neg(1), "b": 0}), {}), "Nonpositive weights")
|
||||
check(E.eq(Math.normalize_distribution({"a": 2, "b": 6}), {"a": E.of("0.25"), "b": E.of("0.75")}), "Normalize weights")
|
||||
check(E.eq(Math.production_step([E.of("0.25"), E.of("0.1"), 2]), 20), "Common whole production step")
|
||||
check(E.eq(Math.production_step([]), 1), "Free production step")
|
||||
var target: Dictionary = {}
|
||||
var produced: float = 0.0
|
||||
var produced: Variant = 0
|
||||
for i in 10:
|
||||
produced += Math.accumulate_whole(target, "remainders", "a", 0.1)
|
||||
check(produced == 1.0, "Fractional production carries")
|
||||
produced = E.add(produced, Math.accumulate_whole(target, "remainders", "a", E.of("0.1")))
|
||||
check(E.eq(produced, 1), "Fractional production carries")
|
||||
var remainders: Dictionary = {}
|
||||
var totals: Dictionary = {"a": 0.0, "b": 0.0, "c": 0.0}
|
||||
var totals: Dictionary = {"a": 0, "b": 0, "c": 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 result: Dictionary = Math.split(remainders, "split", 1, {"a": 1, "b": 1, "c": 1})
|
||||
check(E.eq(Math.sum(result), 1), "Split conserves count")
|
||||
for id in result: totals[id] = E.add(totals[id], result[id])
|
||||
check(E.eq(totals, {"a": 10, "b": 10, "c": 10}), "Rounding debt distributes over time")
|
||||
produced = 0
|
||||
for i in 10: produced = E.add(produced, Math.accumulate_rate(remainders, "rate", 1, E.of("0.1")))
|
||||
check(E.eq(produced, 1), "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")
|
||||
check(E.eq(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(E.eq(catalog.automatic_producers.has(entity), (E.ne(entity.product_per_year_type, ""))), "Producer index")
|
||||
check(E.eq(catalog.unlockable_entities.has(entity), ((not entity.unlock.is_empty()))), "Unlock index")
|
||||
if E.ne(entity.group_as, ""):
|
||||
check((catalog.groups[entity.group_as].has(id) and catalog.members(entity.group_as).has(id)), "Group index")
|
||||
if E.ne(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((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)
|
||||
print(("Quantity and catalog: %d checks, %d failures" % [checks, failures]))
|
||||
quit((1 if failures else 0))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
@@ -6,20 +7,20 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture(legacy: bool):
|
||||
var world = load("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd").new()
|
||||
var world = load(("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd")).new()
|
||||
for id in root.get_node("Global").OwniverseEntities:
|
||||
world.catalog.entities[id] = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if world.catalog.entities[id].group_as != "":
|
||||
world.state.amount[world.catalog.entities[id].group_as] = 0.0
|
||||
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(world.catalog.entities[id].group_as, ""):
|
||||
world.state.amount[world.catalog.entities[id].group_as] = 0
|
||||
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -27,52 +28,52 @@ func dispose(world) -> void:
|
||||
world.free()
|
||||
|
||||
func equivalent(a: Variant, b: Variant) -> bool:
|
||||
if a is Dictionary and b is Dictionary:
|
||||
if a.size() != b.size(): return false
|
||||
if ((a is Dictionary) and (b is Dictionary)):
|
||||
if E.ne(a.size(), b.size()): return false
|
||||
for key in a:
|
||||
if not b.has(key) or not equivalent(a[key], b[key]): return false
|
||||
if ((not b.has(key)) or (not equivalent(a[key], b[key]))): return false
|
||||
return true
|
||||
if a is Array and b is Array:
|
||||
if a.size() != b.size(): return false
|
||||
if ((a is Array) and (b is Array)):
|
||||
if E.ne(a.size(), b.size()): return false
|
||||
for i in a.size():
|
||||
if not equivalent(a[i], b[i]): return false
|
||||
if (not equivalent(a[i], b[i])): return false
|
||||
return true
|
||||
if (a is float or a is int) and (b is float or b is int):
|
||||
return absf(float(a) - float(b)) < 1e-6
|
||||
return a == b
|
||||
if ((((a is float) or (a is int))) and (((b is float) or (b is int)))):
|
||||
return E.lt(E.absolute(E.subtract(E.of(a), E.of(b))), E.of("1e-6"))
|
||||
return E.eq(a, b)
|
||||
|
||||
func run() -> void:
|
||||
for parent in ["LRG", "HRG", "RSG", "RHG"]:
|
||||
for year in [101.0, 101.5, 104.75, 1767.3, 3005.0]:
|
||||
for year in [101, E.of("101.5"), E.of("104.75"), E.of("1767.3"), 3005]:
|
||||
var fast = fixture(false)
|
||||
var legacy = fixture(true)
|
||||
fast.state.amount[parent] = 20000.0
|
||||
fast.state.amount.RD = 100000.0
|
||||
fast.state.amount.HP = 50.0
|
||||
fast.state.amount.Met = 100.0
|
||||
fast.state.binds.RD = {"HP": 2000.0, "C0.1": 100.0}
|
||||
fast.state.binds[parent] = {"HP": 10.0}
|
||||
fast.state.batches[parent] = [{"amount": 20000.0, "start_year": 100.3, "end_year": 3000.7}]
|
||||
fast.state.amount[parent] = 20000
|
||||
fast.state.amount.RD = 100000
|
||||
fast.state.amount.HP = 50
|
||||
fast.state.amount.Met = 100
|
||||
fast.state.binds.RD = {"HP": 2000, "C0.1": 100}
|
||||
fast.state.binds[parent] = {"HP": 10}
|
||||
fast.state.batches[parent] = [{"amount": 20000, "start_year": E.of("100.3"), "end_year": E.of("3000.7")}]
|
||||
# Existing short-lived populations test integration with new arrivals.
|
||||
fast.state.amount.SN = 3.0
|
||||
fast.batches.append_batch(fast.state, "SN", 3.0, 100.5, 100.5)
|
||||
if year == 104.75: fast.state.energy = 7.0
|
||||
fast.state.amount.SN = 3
|
||||
fast.batches.append_batch(fast.state, "SN", 3, E.of("100.5"), E.of("100.5"))
|
||||
if E.eq(year, E.of("104.75")): fast.state.energy = 7
|
||||
legacy.state = fast.state.duplicate(true)
|
||||
fast.evolution.evolve_batches(fast.state, year)
|
||||
legacy.evolution.evolve_batches(legacy.state, year)
|
||||
for field in fast.state:
|
||||
check(equivalent(fast.state[field], legacy.state[field]), "%s at %s: %s matches materialized events" % [parent, year, field])
|
||||
check(equivalent(fast.state[field], legacy.state[field]), ("%s at %s: %s matches materialized events" % [parent, year, field]))
|
||||
for id in ["SN", "DSN", "BSN", "HN", "GRB", "SGRB", "LGRB", "HGRB"]:
|
||||
check(fast.state.batches.get(id, []).size() <= 3, "Only surviving event windows remain")
|
||||
check(E.le(fast.state.batches.get(id, []).size(), 3), "Only surviving event windows remain")
|
||||
dispose(fast)
|
||||
dispose(legacy)
|
||||
var fast = fixture(false)
|
||||
var pending: Dictionary = {}
|
||||
fast.batches.append_batch(fast.state, "SN", 10000.0, 0.25, 1667.25, pending, 1667.25)
|
||||
check(fast.state.batches.SN.size() <= 2 and pending.SN.size() == 1, "Completed windows aggregate without transient batch allocation")
|
||||
var total: float = pending.SN[0].amount
|
||||
for batch in fast.state.batches.SN: total += batch.amount
|
||||
check(total == 10000.0, "Direct completions plus survivors conserve event count")
|
||||
fast.batches.append_batch(fast.state, "SN", 10000, E.of("0.25"), E.of("1667.25"), pending, E.of("1667.25"))
|
||||
check((E.le(fast.state.batches.SN.size(), 2) and E.eq(pending.SN.size(), 1)), "Completed windows aggregate without transient batch allocation")
|
||||
var total: Variant = pending.SN[0].amount
|
||||
for batch in fast.state.batches.SN: total = E.add(total, batch.amount)
|
||||
check(E.eq(total, 10000), "Direct completions plus survivors conserve event count")
|
||||
dispose(fast)
|
||||
print("Short-lived events: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Short-lived events: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+155
-154
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
|
||||
@@ -6,9 +7,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -17,16 +18,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -37,226 +38,226 @@ func dispose(world) -> void:
|
||||
func run() -> void:
|
||||
var world = fixture()
|
||||
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
|
||||
check(world.stellar.evolution_products(world.state, world.catalog.entities[id], 3.0).blast == 3.0 * world.catalog.entities[id].blast, "Entity blast determines power: " + id)
|
||||
world.catalog.entities.SN.output_and.blast = 999.0
|
||||
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities[id], 3).blast, E.multiply(3, world.catalog.entities[id].blast)), E.add("Entity blast determines power: ", id))
|
||||
world.catalog.entities.SN.output_and.blast = 999
|
||||
world.catalog.entities.SN.blast = 4
|
||||
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 8.0, "Imported output cannot override or double blast power")
|
||||
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).blast, 8), "Imported output cannot override or double blast power")
|
||||
world.catalog.entities.SN.blast = 0
|
||||
check(not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
|
||||
world.catalog.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50.0}
|
||||
check(not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).has("blast"), "Alternative output cannot restore disabled blast")
|
||||
check((not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).has("blast")), "Zero entity blast disables external event")
|
||||
world.catalog.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50}
|
||||
check((not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).has("blast")), "Alternative output cannot restore disabled blast")
|
||||
dispose(world)
|
||||
world = fixture()
|
||||
var rd = world.catalog.entities.RD
|
||||
check(rd.planets.HP.chance == 0.01, "Preserve fractional planet probability")
|
||||
check(rd.planets.GG.amount == 2.0 and rd.planets.GG.chance == 0.08, "Read count and chance separately")
|
||||
check(rd._parse_planet_string("HP:1:0.01") == {"HP": {"amount": 1.0, "chance": 0.01}}, "Accept a single planet term")
|
||||
check(E.eq(rd.planets.HP.chance, E.of("0.01")), "Preserve fractional planet probability")
|
||||
check((E.eq(rd.planets.GG.amount, 2) and E.eq(rd.planets.GG.chance, E.of("0.08"))), "Read count and chance separately")
|
||||
check(E.eq(rd._parse_planet_string("HP:1:0.01"), {"HP": {"amount": 1, "chance": E.of("0.01")}}), "Accept a single planet term")
|
||||
check(rd._parse_planet_string("").is_empty(), "Empty planet definitions stay empty")
|
||||
for id in ["HP", "EON1", "C0.9", "C1.2"]:
|
||||
check(world.catalog.entities[id].blastability, "Blast vulnerable: " + id)
|
||||
check(world.catalog.entities[id].blastability, E.add("Blast vulnerable: ", id))
|
||||
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
|
||||
check(not world.catalog.entities[id].blastability, "Blast protected: " + id)
|
||||
check((not world.catalog.entities[id].blastability), E.add("Blast protected: ", id))
|
||||
|
||||
# Actual GI table: HRG -> SN, with 1% GRB; no random rounding.
|
||||
var totals: Dictionary = {}
|
||||
for i in 100:
|
||||
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1.0)
|
||||
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1)
|
||||
for id in products:
|
||||
totals[id] = totals.get(id, 0.0) + products[id]
|
||||
check(totals.get("SN") == 99.0 and totals.get("GRB") == 1.0, "GI rare outcomes accumulate deterministically")
|
||||
totals[id] = E.add(totals.get(id, 0), products[id])
|
||||
check((E.eq(totals.get("SN"), 99) and E.eq(totals.get("GRB"), 1)), "GI rare outcomes accumulate deterministically")
|
||||
var restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
check(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37.0) == restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37.0), "Evolution carry survives serialization")
|
||||
restored.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 37), restored.stellar.evolution_products(restored.state, restored.catalog.entities.HRG, 37)), "Evolution carry survives serialization")
|
||||
dispose(restored)
|
||||
|
||||
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.RD = 4.0
|
||||
world.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||
var lifespan: float = rd.lifespan
|
||||
var width: float = rd.lifespan_delta
|
||||
world.state.amount.N = 0
|
||||
world.state.amount.RD = 4
|
||||
world.batches.append_batch(world.state, "RD", 4, 0, 0)
|
||||
var lifespan: Variant = rd.lifespan
|
||||
var width: Variant = rd.lifespan_delta
|
||||
world.evolution.evolve_batches(world.state, lifespan)
|
||||
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
|
||||
world.evolution.evolve_batches(world.state, lifespan + width / 4.0)
|
||||
check(world.state.amount.RD == 3.0 and world.state.amount.TWD == 1.0, "One quarter of a batch evolves")
|
||||
world.evolution.evolve_batches(world.state, lifespan + width / 2.0)
|
||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Half the batch evolves")
|
||||
world.evolution.evolve_batches(world.state, lifespan + width)
|
||||
check(world.state.amount.RD == 0.0 and world.state.amount.TWD == 4.0, "Window end completes evolution")
|
||||
check(E.eq(world.state.amount.RD, 4), "Window start does not kill the whole batch")
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 4)))
|
||||
check((E.eq(world.state.amount.RD, 3) and E.eq(world.state.amount.TWD, 1)), "One quarter of a batch evolves")
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 2)))
|
||||
check((E.eq(world.state.amount.RD, 2) and E.eq(world.state.amount.TWD, 2)), "Half the batch evolves")
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, width))
|
||||
check((E.eq(world.state.amount.RD, 0) and E.eq(world.state.amount.TWD, 4)), "Window end completes evolution")
|
||||
check(world.state.batches.RD.is_empty(), "Completed batch removed")
|
||||
dispose(world)
|
||||
# Creation accumulates independently for each star/planet pair.
|
||||
world = fixture()
|
||||
world.state.amount.RD = 100.0
|
||||
world.state.amount.RD = 100
|
||||
for i in 100:
|
||||
world.stellar.create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
|
||||
check(world.state.binds.RD.HP == 1.0, "100 RD at metallicity 1 produce one HP")
|
||||
check(world.state.binds.RD.RP == 21.0, "Rocky planet probability retains fractions")
|
||||
check(world.state.binds.RD.GG == 16.0 and world.state.binds.RD.AB == 120.0, "Planet multiplicity applied")
|
||||
check(world.state.amount.HP == 0.0 and world.state.stats.total_amount.HP == 1.0, "Created planets are bound, counted once")
|
||||
world.stellar.create_planets(world.state, "RD", 1, 0, 0, 1)
|
||||
check(E.eq(world.state.binds.RD.HP, 1), "100 RD at metallicity 1 produce one HP")
|
||||
check(E.eq(world.state.binds.RD.RP, 21), "Rocky planet probability retains fractions")
|
||||
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(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Planet carry save validates")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||
restored = fixture()
|
||||
restored.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world.stellar.create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
||||
restored.stellar.create_planets(restored.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
||||
check(world.state == restored.state, "Planet carry continues after save")
|
||||
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"))
|
||||
restored.stellar.create_planets(restored.state, "RD", 37, 0, 0, E.of("0.7"))
|
||||
check(E.eq(world.state, restored.state), "Planet carry continues after save")
|
||||
dispose(restored)
|
||||
dispose(world)
|
||||
|
||||
# A partially dying dwarf sterilizes HP; survivors and RP move to its heir.
|
||||
world = fixture()
|
||||
world.state.amount.RD = 4.0
|
||||
world.state.binds.RD = {"HP": 8.0, "RP": 4.0, "C1.2": 4.0, "C1.3": 4.0, "GG": 4.0}
|
||||
world.batches.append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
||||
world.evolution.evolve_batches(world.state, lifespan + width / 2.0)
|
||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Partial star transition with planets")
|
||||
check(world.state.binds.RD.HP + world.state.binds.TWD.HP == 4.0, "Half of HP sterilized")
|
||||
check(world.state.binds.RD["C1.3"] == 2.0 and world.state.binds.TWD["C1.3"] == 2.0, "Protected civilization splits between parent and heir")
|
||||
check(world.state.binds.RD.RP + world.state.binds.TWD.RP == 10.0, "Sterilized planets become RP without loss")
|
||||
check(world.state.amount.RP == 0.0 and world.state.amount["C1.3"] == 0.0, "Inherited planets never become free")
|
||||
world.evolution.evolve_batches(world.state, lifespan + width)
|
||||
check(world.state.binds.RD["C1.3"] == 0.0 and world.state.binds.TWD["C1.3"] == 4.0, "No planets left at extinct parent type")
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Inherited state validates")
|
||||
world.state.amount.RD = 4
|
||||
world.state.binds.RD = {"HP": 8, "RP": 4, "C1.2": 4, "C1.3": 4, "GG": 4}
|
||||
world.batches.append_batch(world.state, "RD", 4, 0, 0)
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 2)))
|
||||
check((E.eq(world.state.amount.RD, 2) and E.eq(world.state.amount.TWD, 2)), "Partial star transition with planets")
|
||||
check(E.eq(E.add(world.state.binds.RD.HP, world.state.binds.TWD.HP), 4), "Half of HP sterilized")
|
||||
check((E.eq(world.state.binds.RD["C1.3"], 2) and E.eq(world.state.binds.TWD["C1.3"], 2)), "Protected civilization splits between parent and heir")
|
||||
check(E.eq(E.add(world.state.binds.RD.RP, world.state.binds.TWD.RP), 10), "Sterilized planets become RP without loss")
|
||||
check((E.eq(world.state.amount.RP, 0) and E.eq(world.state.amount["C1.3"], 0)), "Inherited planets never become free")
|
||||
world.evolution.evolve_batches(world.state, E.add(lifespan, width))
|
||||
check((E.eq(world.state.binds.RD["C1.3"], 0) and E.eq(world.state.binds.TWD["C1.3"], 4)), "No planets left at extinct parent type")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.WS = 2.0
|
||||
world.state.binds.WS = {"HP": 4.0, "C1.3": 4.0, "RP": 4.0, "GG": 4.0}
|
||||
world.batches.append_batch(world.state, "WS", 2.0, 0.0, 0.0)
|
||||
world.evolution.evolve_batches(world.state, world.catalog.entities.WS.lifespan + world.catalog.entities.WS.lifespan_delta)
|
||||
world.state.amount.WS = 2
|
||||
world.state.binds.WS = {"HP": 4, "C1.3": 4, "RP": 4, "GG": 4}
|
||||
world.batches.append_batch(world.state, "WS", 2, 0, 0)
|
||||
world.evolution.evolve_batches(world.state, E.add(world.catalog.entities.WS.lifespan, world.catalog.entities.WS.lifespan_delta))
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
check(physical.HP == 0.0 and physical["C1.3"] == 0.0 and physical.RP == 0.0 and physical.GG == 0.0, "Level 2 removes all planets, including protected civilization")
|
||||
check(world.state.amount.LRG == 2.0, "Giant evolves after local destruction")
|
||||
check((((E.eq(physical.HP, 0) and E.eq(physical["C1.3"], 0)) and E.eq(physical.RP, 0)) and E.eq(physical.GG, 0)), "Level 2 removes all planets, including protected civilization")
|
||||
check(E.eq(world.state.amount.LRG, 2), "Giant evolves after local destruction")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 10.0
|
||||
world.state.binds.RD = {"HP": 10.0, "C1.2": 10.0, "C1.3": 10.0, "RP": 0.0}
|
||||
world.stellar.apply_blast(world.state, 5.0, {"blast": 1.0, "density": 1.0})
|
||||
check(world.state.binds.RD.HP == 5.0 and world.state.binds.RD["C1.2"] == 5.0, "Blast damages vulnerable worlds proportionally")
|
||||
check(world.state.binds.RD["C1.3"] == 10.0 and world.state.binds.RD.RP == 10.0, "Blast protects C1.3 and creates RP")
|
||||
world.stellar.apply_blast(world.state, 1e6, {"blast": 1.0, "density": 1.0})
|
||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 20.0, "Overwhelming blast is capped at available planets")
|
||||
check(not world.state.amount.has("blast"), "Blast is not a resource")
|
||||
world.state.amount.RD = 10
|
||||
world.state.binds.RD = {"HP": 10, "C1.2": 10, "C1.3": 10, "RP": 0}
|
||||
world.stellar.apply_blast(world.state, 5, {"blast": 1, "density": 1})
|
||||
check((E.eq(world.state.binds.RD.HP, 5) and E.eq(world.state.binds.RD["C1.2"], 5)), "Blast damages vulnerable worlds proportionally")
|
||||
check((E.eq(world.state.binds.RD["C1.3"], 10) and E.eq(world.state.binds.RD.RP, 10)), "Blast protects C1.3 and creates RP")
|
||||
world.stellar.apply_blast(world.state, 1000000, {"blast": 1, "density": 1})
|
||||
check((E.eq(world.state.binds.RD.HP, 0) and E.eq(world.state.binds.RD.RP, 20)), "Overwhelming blast is capped at available planets")
|
||||
check((not world.state.amount.has("blast")), "Blast is not a resource")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.binds.RD = {"HP": 1.0}
|
||||
world.state.amount.RD = 1
|
||||
world.state.binds.RD = {"HP": 1}
|
||||
for i in 10:
|
||||
world.stellar.apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
|
||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 1.0, "Small blast effects accumulate across cycles")
|
||||
world.stellar.apply_blast(world.state, 1, {"blast": 1, "density": E.of("0.1")})
|
||||
check((E.eq(world.state.binds.RD.HP, 0) and E.eq(world.state.binds.RD.RP, 1)), "Small blast effects accumulate across cycles")
|
||||
dispose(world)
|
||||
|
||||
# Actual supernova output: one SN gives a neutron star, resources, and an event.
|
||||
world = fixture()
|
||||
world.state.amount.SN = 1.0
|
||||
world.batches.append_batch(world.state, "SN", 1.0, 0.0, 0.0)
|
||||
world.evolution.evolve_batches(world.state, 2.0)
|
||||
check(world.state.amount.SN == 0.0 and world.state.amount.LNS == 1.0, "Supernova produces a remnant")
|
||||
check(world.state.amount.H == 8.0 and world.state.amount.He == 50.0 and world.state.amount.Met == 11.0, "Supernova returns table resources")
|
||||
check(not world.state.amount.has("blast"), "Supernova dispatches blast output")
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Blast state validates")
|
||||
world.state.amount.SN = 1
|
||||
world.batches.append_batch(world.state, "SN", 1, 0, 0)
|
||||
world.evolution.evolve_batches(world.state, 2)
|
||||
check((E.eq(world.state.amount.SN, 0) and E.eq(world.state.amount.LNS, 1)), "Supernova produces a remnant")
|
||||
check(((E.eq(world.state.amount.H, 8) and E.eq(world.state.amount.He, 50)) and E.eq(world.state.amount.Met, 11)), "Supernova returns table resources")
|
||||
check((not world.state.amount.has("blast")), "Supernova dispatches blast output")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.H = 1000.0
|
||||
world.state.amount.Met = 100.0
|
||||
check(world.production.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
|
||||
check(world.state.binds.RD.HP >= 1.0, "Manual creation dispatches planet formation")
|
||||
check(world.state.amount.H == 900.0, "Planet creation does not duplicate or spend stellar fuel")
|
||||
world.state.amount.H = 1000
|
||||
world.state.amount.Met = 100
|
||||
check(E.eq(world.production.produce("RD", 100), 100), "Manual star creation succeeds")
|
||||
check(E.ge(world.state.binds.RD.HP, 1), "Manual creation dispatches planet formation")
|
||||
check(E.eq(world.state.amount.H, 900), "Planet creation does not duplicate or spend stellar fuel")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.RD = 1.0
|
||||
world.state.amount.H = 100.0
|
||||
world.state.amount.Met = 100.0
|
||||
world.state.auto_resources.H = 100.0
|
||||
world.evolute(0.1)
|
||||
check(world.state.amount.RD == 101.0 and world.state.amount.H == 0.0, "Automatic formation uses its resource budget")
|
||||
check(world.state.binds.RD.HP >= 1.0, "Automatic formation also creates planets")
|
||||
world.state.amount.N = 0
|
||||
world.state.amount.RD = 1
|
||||
world.state.amount.H = 100
|
||||
world.state.amount.Met = 100
|
||||
world.state.auto_resources.H = 100
|
||||
world.evolute(E.of("0.1"))
|
||||
check((E.eq(world.state.amount.RD, 101) and E.eq(world.state.amount.H, 0)), "Automatic formation uses its resource budget")
|
||||
check(E.ge(world.state.binds.RD.HP, 1), "Automatic formation also creates planets")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.catalog.entities.N.auto = 1.0
|
||||
world.catalog.entities.N.auto = 1
|
||||
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
|
||||
world.evolute(100.0)
|
||||
check(world.state.amount.HBD == 10.0, "Brown dwarfs form without existing ordinary stars")
|
||||
check(world.state.amount.H == 999.0 and world.state.auto_resources.H == 999.0, "Brown dwarfs consume exactly their reserved hydrogen")
|
||||
check(world.presentation.amounts(world.state, world.catalog).BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
|
||||
world.catalog.entities.N.product_per_year_amount = 0.0
|
||||
world.evolute(100.0)
|
||||
check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly")
|
||||
world.evolute(100)
|
||||
check(E.eq(world.state.amount.HBD, 10), "Brown dwarfs form without existing ordinary stars")
|
||||
check((E.eq(world.state.amount.H, 999) and E.eq(world.state.auto_resources.H, 999)), "Brown dwarfs consume exactly their reserved hydrogen")
|
||||
check((E.eq(world.presentation.amounts(world.state, world.catalog).BD, 10) and E.gt(world.state.batches.HBD.size(), 0)), "Brown dwarfs appear in counter and evolution batches")
|
||||
world.catalog.entities.N.product_per_year_amount = 0
|
||||
world.evolute(100)
|
||||
check(E.eq(world.state.amount.HBD, 10), "Stored automatic budget is not taxed repeatedly")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.catalog.entities.N.auto = 1.0
|
||||
world.state.amount.RD = 1.0
|
||||
world.evolute(50.0)
|
||||
check(world.state.amount.HBD == 5.0 and is_equal_approx(world.state.auto_star_deficits.HBD_fuel, 0.5), "Individual dwarfs retain prepaid fractional fuel")
|
||||
check(world.state.amount.H == 0.0, "Prepaid fuel is not available to ordinary stars")
|
||||
world.catalog.entities.N.auto = 1
|
||||
world.state.amount.RD = 1
|
||||
world.evolute(50)
|
||||
check((E.eq(world.state.amount.HBD, 5) and E.near(world.state.auto_star_deficits.HBD_fuel, E.of("0.5"))), "Individual dwarfs retain prepaid fractional fuel")
|
||||
check(E.eq(world.state.amount.H, 0), "Prepaid fuel is not available to ordinary stars")
|
||||
var resumed = fixture()
|
||||
resumed.catalog.entities.N.auto = 1.0
|
||||
resumed.state = bytes_to_var(var_to_bytes(world.state))
|
||||
world.evolute(50.0)
|
||||
resumed.evolute(50.0)
|
||||
check(world.state.amount.HBD == 10.0, "Accumulated quota eventually creates a whole batch")
|
||||
check(world.state == resumed.state, "Brown dwarf quota survives serialization deterministically")
|
||||
check(world.state.amount.RD == 1000.0 and world.state.amount.H == 0.0, "Shared automatic hydrogen is spent once")
|
||||
world.state.batches.HBD = [{"amount": 10.0, "start_year": 101.0, "end_year": 101.0}]
|
||||
world.evolution.evolve_batches(world.state, 101.0)
|
||||
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.presentation.amounts(world.state, world.catalog).BD == 10.0, "Cooling preserves aggregate brown dwarf count")
|
||||
resumed.catalog.entities.N.auto = 1
|
||||
resumed.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
world.evolute(50)
|
||||
resumed.evolute(50)
|
||||
check(E.eq(world.state.amount.HBD, 10), "Accumulated quota eventually creates a whole batch")
|
||||
check(E.eq(world.state, resumed.state), "Brown dwarf quota survives serialization deterministically")
|
||||
check((E.eq(world.state.amount.RD, 1000) and E.eq(world.state.amount.H, 0)), "Shared automatic hydrogen is spent once")
|
||||
world.state.batches.HBD = [{"amount": 10, "start_year": 101, "end_year": 101}]
|
||||
world.evolution.evolve_batches(world.state, 101)
|
||||
check(((E.eq(world.state.amount.HBD, 0) and E.eq(world.state.amount.CBD, 10)) and E.eq(world.presentation.amounts(world.state, world.catalog).BD, 10)), "Cooling preserves aggregate brown dwarf count")
|
||||
dispose(resumed)
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
var saw_individual: bool = false
|
||||
for i in range(250):
|
||||
world.state.amount.H += 1.0
|
||||
world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0
|
||||
var before: float = world.state.amount.HBD
|
||||
world.automatic.produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
|
||||
var born: float = world.state.amount.HBD - before
|
||||
check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs")
|
||||
saw_individual = saw_individual or born == 1.0
|
||||
world.state.amount.H = E.add(world.state.amount.H, 1)
|
||||
world.state.auto_resources.H = E.add(world.state.auto_resources.get("H", 0), 1)
|
||||
var before: Variant = world.state.amount.HBD
|
||||
world.automatic.produce_brown_dwarfs(world.state, 1, E.of(i), E.of(E.add(i, 1)))
|
||||
var born: Variant = E.subtract(world.state.amount.HBD, before)
|
||||
check((E.eq(born, 0) or E.eq(born, 1)), "Small fuel increments produce individual dwarfs")
|
||||
saw_individual = (saw_individual or E.eq(born, 1))
|
||||
check(saw_individual, "Random rounding does not wait for ten dwarfs")
|
||||
resumed = fixture()
|
||||
resumed.state = bytes_to_var(var_to_bytes(world.state))
|
||||
resumed.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
for i in range(250, 500):
|
||||
for simulation in [world, resumed]:
|
||||
simulation.state.amount.H += 1.0
|
||||
simulation.state.auto_resources.H += 1.0
|
||||
simulation.automatic.produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
|
||||
check(world.state == resumed.state, "Random sequence and prepaid fuel survive saving")
|
||||
check(is_equal_approx(world.state.amount.H + world.state.amount.HBD * 0.1 + world.state.auto_star_deficits.HBD_fuel, 500.0), "Individual production conserves fuel including prepaid reserve")
|
||||
check(is_equal_approx(world.state.amount.HBD + world.state.auto_star_deficits.HBD, 5.0), "Signed remainder preserves the 0.1 percent allocation")
|
||||
var paused_count: float = world.state.amount.HBD
|
||||
for i in 10: world.automatic.produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
|
||||
check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel")
|
||||
simulation.state.amount.H = E.add(simulation.state.amount.H, 1)
|
||||
simulation.state.auto_resources.H = E.add(simulation.state.auto_resources.H, 1)
|
||||
simulation.automatic.produce_brown_dwarfs(simulation.state, 1, E.of(i), E.of(E.add(i, 1)))
|
||||
check(E.eq(world.state, resumed.state), "Random sequence and prepaid fuel survive saving")
|
||||
check(E.near(E.add(E.add(world.state.amount.H, E.multiply(world.state.amount.HBD, E.of("0.1"))), world.state.auto_star_deficits.HBD_fuel), 500), "Individual production conserves fuel including prepaid reserve")
|
||||
check(E.near(E.add(world.state.amount.HBD, world.state.auto_star_deficits.HBD), 5), "Signed remainder preserves the 0.1 percent allocation")
|
||||
var paused_count: Variant = world.state.amount.HBD
|
||||
for i in 10: world.automatic.produce_brown_dwarfs(world.state, 0, 500, 501)
|
||||
check(E.eq(world.state.amount.HBD, paused_count), "No repeated random attempts without new fuel")
|
||||
dispose(resumed)
|
||||
dispose(world)
|
||||
|
||||
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
|
||||
world = fixture()
|
||||
world.catalog.entities.HRG.destroy_level = 0
|
||||
world.state.amount.HRG = 100.0
|
||||
world.state.binds.HRG = {"C1.3": 100.0}
|
||||
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 100.0}]
|
||||
world.evolution.evolve_batches(world.state, 0.1)
|
||||
check(world.state.binds.SN["C1.3"] == 99.0 and world.state.binds.GRB["C1.3"] == 1.0, "Planets split among actual alternative successors")
|
||||
check(world.state.binds.HRG["C1.3"] == 0.0, "Alternative evolution leaves no orphan bindings")
|
||||
world.state.amount.HRG = 100
|
||||
world.state.binds.HRG = {"C1.3": 100}
|
||||
world.state.batches.HRG = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 100}]
|
||||
world.evolution.evolve_batches(world.state, E.of("0.1"))
|
||||
check((E.eq(world.state.binds.SN["C1.3"], 99) and E.eq(world.state.binds.GRB["C1.3"], 1)), "Planets split among actual alternative successors")
|
||||
check(E.eq(world.state.binds.HRG["C1.3"], 0), "Alternative evolution leaves no orphan bindings")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount.N = 0.0
|
||||
world.state.amount.HRG = 1.0
|
||||
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
||||
world.evolute(3.0)
|
||||
check(world.state.amount.LNS == 1.0 and world.state.amount.SN == 0.0, "Long step follows multiple stellar generations")
|
||||
check(world.state.amount.S >= 99.0, "Short-lived newborn supernova emits S before disappearing")
|
||||
world.state.amount.N = 0
|
||||
world.state.amount.HRG = 1
|
||||
world.state.batches.HRG = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 1}]
|
||||
world.evolute(3)
|
||||
check((E.eq(world.state.amount.LNS, 1) and E.eq(world.state.amount.SN, 0)), "Long step follows multiple stellar generations")
|
||||
check(E.ge(world.state.amount.S, 99), "Short-lived newborn supernova emits S before disappearing")
|
||||
dispose(world)
|
||||
|
||||
print("Stellar rules: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("Stellar rules: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+42
-41
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -18,16 +19,16 @@ func fixture():
|
||||
for id in global.OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
world.catalog.entities["N"].life_product = {}
|
||||
world.catalog.entities["N"].product_per_year_type = "H"
|
||||
world.catalog.entities["N"].product_per_year_amount = 10.0
|
||||
world.catalog.entities["N"].auto = 0.0
|
||||
world.state.amount.N = 1.0
|
||||
world.catalog.entities["N"].product_per_year_amount = 10
|
||||
world.catalog.entities["N"].auto = 0
|
||||
world.state.amount.N = 1
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -38,55 +39,55 @@ func dispose(world) -> void:
|
||||
func run() -> void:
|
||||
var saves: Node = root.get_node("SaveManager")
|
||||
var world = fixture()
|
||||
world.evolute(0.09)
|
||||
check(world.state.time.year == 0.0, "Do not process before a tenth boundary")
|
||||
check(world.state.amount.H == 0.0, "No premature production")
|
||||
world.evolute(0.01)
|
||||
check(world.state.time.year == 0.1, "Process the first tenth")
|
||||
check(world.state.amount.H == 1.0, "Production runs at 0.1 year")
|
||||
world.evolute(0.2)
|
||||
check(world.state.time.year == 0.3 and world.state.amount.H == 3.0, "Do not reprocess previous tenths")
|
||||
world.evolute(E.of("0.09"))
|
||||
check(E.eq(world.state.time.year, 0), "Do not process before a tenth boundary")
|
||||
check(E.eq(world.state.amount.H, 0), "No premature production")
|
||||
world.evolute(E.of("0.01"))
|
||||
check(E.eq(world.state.time.year, E.of("0.1")), "Process the first tenth")
|
||||
check(E.eq(world.state.amount.H, 1), "Production runs at 0.1 year")
|
||||
world.evolute(E.of("0.2"))
|
||||
check((E.eq(world.state.time.year, E.of("0.3")) and E.eq(world.state.amount.H, 3)), "Do not reprocess previous tenths")
|
||||
var before: Dictionary = world.state.duplicate(true)
|
||||
for invalid in [0.0, -1.0, NAN, INF, 1e308]:
|
||||
for invalid in [0, E.neg(1), NAN, INF, E.of("1e308")]:
|
||||
world.evolute(invalid)
|
||||
check(world.state == before, "Invalid/overflow interval rejected")
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Fractional save is valid")
|
||||
check(E.eq(world.state, before), "Invalid/overflow interval rejected")
|
||||
check(E.eq(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||
var invalid_save: Dictionary = world.state.duplicate(true)
|
||||
invalid_save.time.year = 0.25
|
||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}) != "", "Reject inconsistent fractional clock")
|
||||
invalid_save.time.year = E.of("0.25")
|
||||
check(E.ne(saves.validate_data({"format_version": 3, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||
dispose(world)
|
||||
|
||||
for fps in [30, 60, 144]:
|
||||
world = fixture()
|
||||
for frame in fps:
|
||||
world.evolute(1.0 / fps)
|
||||
check(world.state.time.year == 1.0, "One second at %d fps" % fps)
|
||||
check(world.state.amount.H == 10.0, "Production across frames at %d fps" % fps)
|
||||
world.evolute(E.divide(1, fps))
|
||||
check(E.eq(world.state.time.year, 1), ("One second at %d fps" % fps))
|
||||
check(E.eq(world.state.amount.H, 10), ("Production across frames at %d fps" % fps))
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.evolute(0.05)
|
||||
world.evolute(E.of("0.05"))
|
||||
world.speed = 2
|
||||
world.evolute(0.005)
|
||||
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
|
||||
world.evolute(E.of("0.005"))
|
||||
check(E.eq(world.state.time.year, E.of("0.1")), "Speed change preserves accumulated fraction")
|
||||
world.speed = 100
|
||||
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")
|
||||
check((E.eq(world.speed, speed_limit) and E.eq(world.state.speed_in_year, E.power(10, E.subtract(speed_limit, 1)))), "Configured upper speed limit")
|
||||
world.speed = E.neg(10)
|
||||
check((E.eq(world.speed, 1) and E.eq(world.state.speed_in_year, 1)), "Lower speed limit unchanged")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
world.catalog.entities.N.output_and = {"He": 1.0}
|
||||
world.catalog.entities.N.output_and = {"He": 1}
|
||||
world.catalog.entities.N.output_or = {}
|
||||
world.state.batches.N = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
||||
world.evolute(0.1)
|
||||
check(world.state.amount.N == 0.0 and world.state.amount.He == 1.0, "Existing evolution executes at fractional boundary")
|
||||
world.state.batches.N = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 1}]
|
||||
world.evolute(E.of("0.1"))
|
||||
check((E.eq(world.state.amount.N, 0) and E.eq(world.state.amount.He, 1)), "Existing evolution executes at fractional boundary")
|
||||
dispose(world)
|
||||
check(Clock.processed_year(0.099) == 0.0, "Do not round a genuine incomplete tenth forward")
|
||||
check(Clock.processed_year(1e9 + 0.45) == 1e9 + 0.4, "Large ages preserve fractional precision")
|
||||
print("Time state: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
check(E.eq(Clock.processed_year(E.of("0.099")), 0), "Do not round a genuine incomplete tenth forward")
|
||||
check(E.eq(Clock.processed_year(E.add(E.of("1e9"), E.of("0.45"))), E.add(E.of("1e9"), E.of("0.4"))), "Large ages preserve fractional precision")
|
||||
print(("Time state: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+20
-19
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
var checks: int = 0
|
||||
var failures: int = 0
|
||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(value: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not value:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not value):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func run() -> void:
|
||||
@@ -22,39 +23,39 @@ func run() -> void:
|
||||
await process_frame
|
||||
stage.stage_page_scroller.scroll_horizontal = stage.PAGE_WIDTH
|
||||
await process_frame
|
||||
stage.owniverse.state.amount.H = 1000.0
|
||||
stage.owniverse.state.amount.RD = 2.0
|
||||
stage.owniverse.state.amount.H = 1000
|
||||
stage.owniverse.state.amount.RD = 2
|
||||
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||
stage.set_owniverse_item_value()
|
||||
var hydrogen = stage.owniverse_items.H
|
||||
var dwarf = stage.owniverse_items.RD
|
||||
check(hydrogen.description.text == "1 000", "Pending quantities reach controls")
|
||||
check(dwarf.description.text == "2", "Other page receives its current quantity")
|
||||
check(E.eq(hydrogen.description.text, "1 000"), "Pending quantities reach controls")
|
||||
check(E.eq(dwarf.description.text, "2"), "Other page receives its current quantity")
|
||||
check(stage.owniverse.presentation.pending_amounts.is_empty(), "Published queue is consumed by Stage")
|
||||
stage.owniverse.presentation.deferred_updates = true
|
||||
stage.owniverse.state.amount.H = 2000.0
|
||||
stage.owniverse.state.amount.H = 2000
|
||||
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||
check(stage.owniverse.presentation.dirty and stage.owniverse.presentation.pending_amounts.is_empty(), "Deferred refresh marks dirty without republishing")
|
||||
check((stage.owniverse.presentation.dirty and stage.owniverse.presentation.pending_amounts.is_empty()), "Deferred refresh marks dirty without republishing")
|
||||
stage.set_owniverse_item_value()
|
||||
check(hydrogen.description.text == "1 000", "Labels retain last published quantity")
|
||||
check(E.eq(hydrogen.description.text, "1 000"), "Labels retain last published quantity")
|
||||
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog, true)
|
||||
stage.set_owniverse_item_value()
|
||||
check(hydrogen.description.text == "2 000" and not stage.owniverse.presentation.dirty, "Forced refresh publishes latest state")
|
||||
check((E.eq(hydrogen.description.text, "2 000") and (not stage.owniverse.presentation.dirty)), "Forced refresh publishes latest state")
|
||||
stage.owniverse.presentation.deferred_updates = false
|
||||
stage.item_activated(hydrogen)
|
||||
stage.set_owniverse_item_value()
|
||||
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production publishes the new quantity")
|
||||
check(stage.owniverse.state.amount.H > 2000.0, "Production action actually executed")
|
||||
check(E.eq(hydrogen.description.text, root.get_node("Global").format_number(stage.owniverse.state.amount.H)), "Player production publishes the new quantity")
|
||||
check(E.gt(stage.owniverse.state.amount.H, 2000), "Production action actually executed")
|
||||
dwarf.visible = true
|
||||
stage.stage_page_scroller.scroll_horizontal = 2 * stage.PAGE_WIDTH
|
||||
stage.stage_page_scroller.scroll_horizontal = E.multiply(2, stage.PAGE_WIDTH)
|
||||
await process_frame
|
||||
stage._refresh_universe_ui()
|
||||
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
|
||||
stage.owniverse.state.time.year = 1234.5
|
||||
check(E.eq(dwarf.description.text, "2"), "Newly visible page receives cached latest values")
|
||||
stage.owniverse.state.time.year = E.of("1234.5")
|
||||
stage._refresh_universe_ui()
|
||||
check(stage.date_time.text == "1 234.5", "Time displays tenths with thousands separator")
|
||||
check(E.eq(stage.date_time.text, "1 234.5"), "Time displays tenths with thousands separator")
|
||||
for entity in stage.owniverse.catalog.entities.values(): entity.free()
|
||||
stage.owniverse.free()
|
||||
scene.free()
|
||||
print("UI updates: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("UI updates: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+98
-101
@@ -1,4 +1,5 @@
|
||||
extends SceneTree
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
|
||||
|
||||
@@ -11,9 +12,9 @@ func _initialize() -> void:
|
||||
call_deferred("run")
|
||||
|
||||
func check(condition: bool, message: String) -> void:
|
||||
checks += 1
|
||||
if not condition:
|
||||
failures += 1
|
||||
checks = E.add(checks, 1)
|
||||
if (not condition):
|
||||
failures = E.add(failures, 1)
|
||||
push_error(message)
|
||||
|
||||
func fixture():
|
||||
@@ -21,11 +22,11 @@ func fixture():
|
||||
for id in root.get_node("Global").OwniverseEntities:
|
||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
||||
world.catalog.entities[id] = entity
|
||||
world.state.amount[id] = 0.0
|
||||
world.state.stats.total_amount[id] = 0.0
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = 0.0
|
||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
||||
world.state.amount[id] = 0
|
||||
world.state.stats.total_amount[id] = 0
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = 0
|
||||
world.state.stats.total_amount[entity.group_as] = 0
|
||||
return world
|
||||
|
||||
func dispose(world) -> void:
|
||||
@@ -35,116 +36,116 @@ func dispose(world) -> void:
|
||||
func run() -> void:
|
||||
var global: Node = root.get_node("Global")
|
||||
var world = fixture()
|
||||
world.state.amount.RD = 400.0
|
||||
world.batches.append_batch(world.state, "RD", 400.0, 0.0, 0.0)
|
||||
var end: float = world.state.batches.RD[0].end_year
|
||||
world.environment.modify_world_parameter("Bur", 9.0)
|
||||
check(is_equal_approx(world.state.batches.RD[0].end_year, end / 10.0), "Burnability accelerates existing cohorts")
|
||||
var count: float = world.state.batches.RD[0].amount
|
||||
world.environment.modify_world_parameter("Bur", -9.0)
|
||||
check(is_equal_approx(world.state.batches.RD[0].end_year, end) and world.state.batches.RD[0].amount == count, "Changing burnability back preserves population and lifetime")
|
||||
world.environment.modify_world_parameter("Bur", 9.0)
|
||||
world.batches.append_batch(world.state, "YD", 10.0, 0.0, 0.0)
|
||||
check(is_equal_approx(world.state.batches.YD[0].start_year, world.catalog.entities.YD.lifespan / 10.0), "New stars use the same burnability")
|
||||
world.state.time = {"datetime": end / 10.0 - 0.1, "year": end / 10.0 - 0.1}
|
||||
world.state.amount.RD = 400
|
||||
world.batches.append_batch(world.state, "RD", 400, 0, 0)
|
||||
var end: Variant = world.state.batches.RD[0].end_year
|
||||
world.environment.modify_world_parameter("Bur", 9)
|
||||
check(E.near(world.state.batches.RD[0].end_year, E.divide(end, 10)), "Burnability accelerates existing cohorts")
|
||||
var count: Variant = world.state.batches.RD[0].amount
|
||||
world.environment.modify_world_parameter("Bur", E.neg(9))
|
||||
check((E.near(world.state.batches.RD[0].end_year, end) and E.eq(world.state.batches.RD[0].amount, count)), "Changing burnability back preserves population and lifetime")
|
||||
world.environment.modify_world_parameter("Bur", 9)
|
||||
world.batches.append_batch(world.state, "YD", 10, 0, 0)
|
||||
check(E.near(world.state.batches.YD[0].start_year, E.divide(world.catalog.entities.YD.lifespan, 10)), "New stars use the same burnability")
|
||||
world.state.time = {"datetime": E.subtract(E.divide(end, 10), E.of("0.1")), "year": E.subtract(E.divide(end, 10), E.of("0.1"))}
|
||||
world.batches.withdraw_batches(world.state, "RD", world.state.time.year)
|
||||
world.environment.modify_world_parameter("Bur", -18.0)
|
||||
check(world.state.batches.RD[0].end_year >= world.state.time.year, "Slowing a partially evolved cohort keeps its remaining window in the future")
|
||||
world.environment.modify_world_parameter("Exp", 9.0)
|
||||
check(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2.0).blast == 180.0, "Explosion power includes world explosiveness")
|
||||
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000.0)
|
||||
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 900.0, "Explosiveness raises rare-event probability without duplicating stars")
|
||||
world.catalog.entities.HRG.output_or.chance = 50.0
|
||||
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100.0)
|
||||
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 0.0, "Rare-event probability is bounded at one")
|
||||
world.environment.modify_world_parameter("Bur", E.neg(18))
|
||||
check(E.ge(world.state.batches.RD[0].end_year, world.state.time.year), "Slowing a partially evolved cohort keeps its remaining window in the future")
|
||||
world.environment.modify_world_parameter("Exp", 9)
|
||||
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).blast, 180), "Explosion power includes world explosiveness")
|
||||
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000)
|
||||
check((E.eq(products.get("GRB", 0), 100) and E.eq(products.get("SN", 0), 900)), "Explosiveness raises rare-event probability without duplicating stars")
|
||||
world.catalog.entities.HRG.output_or.chance = 50
|
||||
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100)
|
||||
check((E.eq(products.get("GRB", 0), 100) and E.eq(products.get("SN", 0), 0)), "Rare-event probability is bounded at one")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||
world.state.life_remainders.clear()
|
||||
world.environment.modify_world_parameter("Agg", 9.0)
|
||||
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||
check(aggressive.EON1 > ordinary.EON1, "Aggression accelerates primitive evolution")
|
||||
world.environment.modify_world_parameter("Agg", 9)
|
||||
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||
check(E.gt(aggressive.EON1, ordinary.EON1), "Aggression accelerates primitive evolution")
|
||||
world.state.life_remainders.clear()
|
||||
world.state.world_parameters.Order = 10.0
|
||||
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||
check(orderly.RP < aggressive.RP, "Order reduces degradation")
|
||||
world.state.world_parameters.Order = 10
|
||||
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||
check(E.lt(orderly.RP, aggressive.RP), "Order reduces degradation")
|
||||
world.state.life_remainders.clear()
|
||||
world.state.world_parameters.Enlightenment = 10.0
|
||||
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000.0)
|
||||
check(enlightened.EON1 > orderly.EON1, "Enlightenment increases primitive progress")
|
||||
world.state.world_parameters.Enlightenment = 10
|
||||
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||
check(E.gt(enlightened.EON1, orderly.EON1), "Enlightenment increases primitive progress")
|
||||
dispose(world)
|
||||
|
||||
world = fixture()
|
||||
var hotter = fixture()
|
||||
for simulation in [world, hotter]:
|
||||
simulation.state.amount.RD = 1000000.0
|
||||
simulation.batches.append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
|
||||
hotter.environment.modify_world_parameter("Bur", 9.0)
|
||||
world.evolute(1000.0)
|
||||
hotter.evolute(1000.0)
|
||||
check(hotter.state.amount.S > world.state.amount.S, "Burnability changes actual space production")
|
||||
check(hotter.state.energy + hotter.state.amount.S == Parameters.INITIAL_ENERGY, "Enhanced production still spends the GI energy budget")
|
||||
simulation.state.amount.RD = 1000000
|
||||
simulation.batches.append_batch(simulation.state, "RD", 1000000, 0, 0)
|
||||
hotter.environment.modify_world_parameter("Bur", 9)
|
||||
world.evolute(1000)
|
||||
hotter.evolute(1000)
|
||||
check(E.gt(hotter.state.amount.S, world.state.amount.S), "Burnability changes actual space production")
|
||||
check(E.eq(E.add(hotter.state.energy, hotter.state.amount.S), Parameters.INITIAL_ENERGY), "Enhanced production still spends the GI energy budget")
|
||||
dispose(world)
|
||||
dispose(hotter)
|
||||
|
||||
world = fixture()
|
||||
world.state.amount["C0.2"] = 1000000.0
|
||||
world.state.ethics["C0.2"] = {"m": 1000000.0}
|
||||
world.state.amount["C0.2"] = 1000000
|
||||
world.state.ethics["C0.2"] = {"m": 1000000}
|
||||
var baseline: Dictionary = world.state.duplicate(true)
|
||||
var advanced: Dictionary = baseline.duplicate(true)
|
||||
advanced.world_parameters.Agg = 19.0
|
||||
advanced.world_parameters.Agg = 19
|
||||
var next_stage: String = global.CivilizationRules["C0.2"].next
|
||||
var outcomes: Array = []
|
||||
for target in [baseline, advanced]:
|
||||
var expired: Dictionary = {"C0.2": [{"amount": 1000000.0, "from": 0.0, "to": 1.0}]}
|
||||
var expired: Dictionary = {"C0.2": [{"amount": 1000000, "from": 0, "to": 1}]}
|
||||
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
|
||||
CivilizationEvolution.prepare(world, target, expired, 1.0, budget, global.CivilizationRules)
|
||||
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0.0))
|
||||
check(outcomes[1] > outcomes[0], "Aggression changes actual ethical civilization transitions")
|
||||
CivilizationEvolution.prepare(world, target, expired, 1, budget, global.CivilizationRules)
|
||||
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0))
|
||||
check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions")
|
||||
dispose(world)
|
||||
|
||||
# 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.0}, "C2.1": {"d": 100.0}}
|
||||
world.state.ethics = {"C2.0": {"d": 100}, "C2.1": {"d": 100}}
|
||||
var source: Dictionary = world.state.duplicate(true)
|
||||
var single: Dictionary = source.duplicate(true)
|
||||
var split: Dictionary = source.duplicate(true)
|
||||
Parameters.advance(source, single, rules, 1.0e6)
|
||||
for i in 10: Parameters.advance(source, split, rules, 1.0e5)
|
||||
check(is_equal_approx(single.trends.Integration, 0.018), "Trend integrates elapsed years and doubled stage weights")
|
||||
Parameters.advance(source, single, rules, 1000000)
|
||||
for i in 10: Parameters.advance(source, split, rules, 100000)
|
||||
check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights")
|
||||
for key in Parameters.TRENDS:
|
||||
check(is_equal_approx(single.trends[key], split.trends[key]), "Trend is independent of frame partition: " + key)
|
||||
source.ethics["C2.0"].d = 1000000.0
|
||||
check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key))
|
||||
source.ethics["C2.0"].d = 1000000
|
||||
var larger: Dictionary = world.state.duplicate(true)
|
||||
Parameters.advance(source, larger, rules, 1.0e6)
|
||||
check(is_equal_approx(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
||||
world.state.trends.Integration = 0.99
|
||||
Parameters.advance(source, world.state, rules, 1.0e8)
|
||||
check(world.state.civilization_stats["trend:Integration"] == 2.0, "Long steps process every crossed threshold")
|
||||
check(is_equal_approx(world.state.trends.Integration, 0.79), "Threshold crossing preserves the fractional remainder")
|
||||
check(world.state.world_parameters.Federation > 1.0 or world.state.world_parameters.Enlightenment > 1.0 or world.state.world_parameters.Order > 1.0, "Integration events change the world")
|
||||
var restored: Dictionary = bytes_to_var(var_to_bytes(world.state))
|
||||
Parameters.advance(source, world.state, rules, 1.0e8)
|
||||
Parameters.advance(source, restored, rules, 1.0e8)
|
||||
check(world.state == restored, "Trend events resume identically after serialization")
|
||||
Parameters.advance(source, world.state, rules, 1.0e12)
|
||||
check(world.state.trends.Integration >= 0.0 and world.state.trends.Integration < 1.0, "Very large intervals finish with a valid trend fraction")
|
||||
check(world.state.world_parameters.Agg >= 1.0, "Repeated integration events cannot reduce aggression below the scale")
|
||||
var payload: Dictionary = {"format_version": 2, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
Parameters.advance(source, larger, rules, 1000000)
|
||||
check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
||||
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")
|
||||
check(E.near(world.state.trends.Integration, E.of("0.79")), "Threshold crossing preserves the fractional remainder")
|
||||
check(((E.gt(world.state.world_parameters.Federation, 1) or E.gt(world.state.world_parameters.Enlightenment, 1)) or E.gt(world.state.world_parameters.Order, 1)), "Integration events change the world")
|
||||
var restored: Dictionary = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||
Parameters.advance(source, world.state, rules, 100000000)
|
||||
Parameters.advance(source, restored, rules, 100000000)
|
||||
check(E.eq(world.state, restored), "Trend events resume identically after serialization")
|
||||
Parameters.advance(source, world.state, rules, E.of("1.0e12"))
|
||||
check((E.ge(world.state.trends.Integration, 0) and E.lt(world.state.trends.Integration, 1)), "Very large intervals finish with a valid trend fraction")
|
||||
check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale")
|
||||
var payload: Dictionary = {"format_version": 3, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||
var manager: Node = root.get_node("SaveManager")
|
||||
check(manager.validate_data(payload) == "", "New parameters and trends pass save validation")
|
||||
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
||||
var old: Dictionary = payload.duplicate(true)
|
||||
old.state.erase("world_parameters")
|
||||
old.state.erase("trends")
|
||||
check(manager.validate_data(old) == "", "Existing current-format saves remain readable")
|
||||
payload.state.world_parameters.Agg = 20.0
|
||||
check(manager.validate_data(payload) != "", "Out-of-range scales are rejected")
|
||||
check(E.eq(manager.validate_data(old), ""), "Existing current-format saves remain readable")
|
||||
payload.state.world_parameters.Agg = 20
|
||||
check(E.ne(manager.validate_data(payload), ""), "Out-of-range scales are rejected")
|
||||
dispose(world)
|
||||
|
||||
for sample in [[1e-7, 0.0], [1e-6, 0.5], [1e-5, 1.0], [0.9, 0.0], [0.95, 0.5], [1.0, 1.0]]:
|
||||
check(is_equal_approx(Parameters.density_display(sample[0]).position, sample[1]), "Density boundary " + str(sample[0]))
|
||||
for sample in [[E.of("1e-7"), 0], [E.of("1e-6"), E.of("0.5")], [E.of("1e-5"), 1], [E.of("0.9"), 0], [E.of("0.95"), E.of("0.5")], [1, 1]]:
|
||||
check(E.near(Parameters.density_display(sample[0]).position, sample[1]), E.add("Density boundary ", str(sample[0])))
|
||||
# The fixture overrides saving/loading; the player's save is never accessed.
|
||||
var scene = load("res://world/world.tscn").instantiate()
|
||||
var stage = scene.get_node("Stage")
|
||||
@@ -153,40 +154,36 @@ func run() -> void:
|
||||
root.add_child(scene)
|
||||
await process_frame
|
||||
await process_frame
|
||||
stage.owniverse.state.energy = Parameters.INITIAL_ENERGY / 4.0
|
||||
stage.owniverse.state.world_parameters.Bur = 19.0
|
||||
stage.owniverse.state.trends.Integration = 0.75
|
||||
stage.owniverse.state.energy = E.divide(Parameters.INITIAL_ENERGY, 4)
|
||||
stage.owniverse.state.world_parameters.Bur = 19
|
||||
stage.owniverse.state.trends.Integration = E.of("0.75")
|
||||
stage.world_indicators.update(stage.owniverse)
|
||||
var warning: TextureRect = scene.get_node("Background/WarningBorder")
|
||||
check(warning.mouse_filter == Control.MOUSE_FILTER_IGNORE, "Warning overlay does not block input")
|
||||
var environments: Array = [
|
||||
[1.0e-7, "ff0000ff"], [1.0e-6, "df2020bf"], [1.0e-5, "c040407f"],
|
||||
[0.1, "00000000"], [0.9, "40a0c07f"], [0.97, "20b0dfbf"], [1.0, "00c0ffff"],
|
||||
[0.1, "00000000"],
|
||||
]
|
||||
check(E.eq(warning.mouse_filter, Control.MOUSE_FILTER_IGNORE), "Warning overlay does not block input")
|
||||
var environments: Array = [[E.of("1.0e-7"), "ff0000ff"], [E.of("1.0e-6"), "df2020bf"], [E.of("1.0e-5"), "c040407f"], [E.of("0.1"), "00000000"], [E.of("0.9"), "40a0c07f"], [E.of("0.97"), "20b0dfbf"], [1, "00c0ffff"], [E.of("0.1"), "00000000"]]
|
||||
for sample in environments:
|
||||
check(Parameters.density_warning_color(sample[0]).is_equal_approx(Color.html(sample[1])), "Unity warning threshold: " + str(sample[0]))
|
||||
stage.owniverse.state.amount.H = 1e14
|
||||
check(Parameters.density_warning_color(sample[0]).is_equal_approx(Color.html(sample[1])), E.add("Unity warning threshold: ", str(sample[0])))
|
||||
stage.owniverse.state.amount.H = E.of("1e14")
|
||||
stage.world_indicators.update(stage.owniverse)
|
||||
check(warning.self_modulate.is_equal_approx(Color.html("00c0ffff")), "Compression warning uses current universe density")
|
||||
stage.owniverse.state.amount.H = 0.0
|
||||
stage.owniverse.state.amount.S = 1e24
|
||||
stage.owniverse.state.amount.H = 0
|
||||
stage.owniverse.state.amount.S = E.of("1e24")
|
||||
stage.world_indicators.update(stage.owniverse)
|
||||
check(warning.self_modulate.is_equal_approx(Color.html("ff0000ff")), "Rip warning replaces compression warning")
|
||||
stage.owniverse.state.amount.S = 0.0
|
||||
stage.owniverse.state.amount.S = 0
|
||||
stage.world_indicators.update(stage.owniverse)
|
||||
check(warning.self_modulate.a == 0.0, "Warning clears when density returns to normal")
|
||||
check(E.eq(warning.self_modulate.a, 0), "Warning clears when density returns to normal")
|
||||
for bar in stage.world_indicators.energy_bars:
|
||||
check(bar.scale.x == 0.25, "Energy fill is synchronized on both pages")
|
||||
check(E.eq(bar.scale.x, E.of("0.25")), "Energy fill is synchronized on both pages")
|
||||
var pointer: Control = stage.world_indicators.pointers.Bur
|
||||
check(is_equal_approx(pointer.position.x + pointer.size.x / 2.0, 304.0), "Burnability pointer reaches the correct scale endpoint")
|
||||
check(stage.world_indicators.trend_bars[1].scale.y == 0.75, "Trend bar displays its stored level")
|
||||
check(E.near(E.add(pointer.position.x, E.divide(pointer.size.x, 2)), 304), "Burnability pointer reaches the correct scale endpoint")
|
||||
check(E.eq(stage.world_indicators.trend_bars[1].scale.y, E.of("0.75")), "Trend bar displays its stored level")
|
||||
for locale in ["en", "ru"]:
|
||||
global.Locale = locale
|
||||
for id in ["Energy", "Density", "Burnability", "Explosiveness", "Aggression", "Trends"]:
|
||||
var description: String = Descriptions.render(stage.owniverse, global, id, "Indicator")
|
||||
check(not description.contains("—") and not description.contains("%value"), "Resolved indicator description: " + locale + "/" + id)
|
||||
check(((not description.contains("—")) and (not description.contains("%value"))), E.add(E.add(E.add("Resolved indicator description: ", locale), "/"), id))
|
||||
dispose(stage.owniverse)
|
||||
scene.free()
|
||||
print("World indicators: %d checks, %d failures" % [checks, failures])
|
||||
quit(0 if failures == 0 else 1)
|
||||
print(("World indicators: %d checks, %d failures" % [checks, failures]))
|
||||
quit((0 if E.eq(failures, 0) else 1))
|
||||
|
||||
+28
-39
@@ -1,29 +1,18 @@
|
||||
extends HBoxContainer
|
||||
class_name OwniverseAction
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
signal action_pressed(page_id, amount: float)
|
||||
signal action_pressed ( page_id , amount : Variant )
|
||||
|
||||
@onready var action_labels: Array = [
|
||||
$ActionControl0/Label,
|
||||
$ActionControl1/Label,
|
||||
$ActionControl2/Label,
|
||||
$ActionControl3/Label,
|
||||
$ActionControl4/Label,
|
||||
]
|
||||
@onready var action_labels : Array = [$ActionControl0/Label, $ActionControl1/Label, $ActionControl2/Label, $ActionControl3/Label, $ActionControl4/Label]
|
||||
|
||||
var page_id: int = -1
|
||||
var page_id: int = E.neg(1)
|
||||
var current_item_id = ""
|
||||
|
||||
var action_values: Dictionary = {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
}
|
||||
var action_values: Dictionary = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0}
|
||||
|
||||
|
||||
func set_actions(item_id: String, value: float, preview: Callable = Callable()) -> void:
|
||||
func set_actions(item_id: String, value: Variant, preview: Callable = Callable()) -> void:
|
||||
current_item_id = item_id
|
||||
var requests: Array = _get_distribution(value)
|
||||
var unique_requests: Array = []
|
||||
@@ -31,32 +20,32 @@ func set_actions(item_id: String, value: float, preview: Callable = Callable())
|
||||
var labels: Dictionary = {}
|
||||
# Prefer the largest request (including MAX) when previews or rounded
|
||||
# scientific labels coincide. Keep the request paired with its preview.
|
||||
for i in range(requests.size() - 1, -1, -1):
|
||||
if requests[i] <= 0.0: continue
|
||||
var amount: float = preview.call(requests[i]) if preview.is_valid() else requests[i]
|
||||
if not is_finite(amount) or amount < 1.0 or amount != floorf(amount): continue
|
||||
for i in range(E.subtract(requests.size(), 1), E.neg(1), E.neg(1)):
|
||||
if E.le(requests[i], 0): continue
|
||||
var amount: Variant = (preview.call(requests[i]) if preview.is_valid() else requests[i])
|
||||
if (((not E.finite(amount)) or E.lt(amount, 1)) or E.ne(amount, E.floor_value(amount))): continue
|
||||
var text: String = _format_action_value(amount)
|
||||
if displayed.has(amount) or labels.has(text): continue
|
||||
if (E.contains(displayed, amount) or labels.has(text)): continue
|
||||
labels[text] = true
|
||||
unique_requests.push_front(requests[i])
|
||||
displayed.push_front(amount)
|
||||
while unique_requests.size() < 5:
|
||||
unique_requests.append(0.0)
|
||||
displayed.append(0.0)
|
||||
while E.lt(unique_requests.size(), 5):
|
||||
unique_requests.append(0)
|
||||
displayed.append(0)
|
||||
_set_action_values(unique_requests, displayed)
|
||||
|
||||
func _get_distribution(value: float) -> Array:
|
||||
if value < 1.0 or not is_finite(value):
|
||||
return [0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
value = floorf(value)
|
||||
func _get_distribution(value: Variant) -> Array:
|
||||
if (E.lt(value, 1) or (not E.finite(value))):
|
||||
return [0, 0, 0, 0, 0]
|
||||
value = E.floor_value(value)
|
||||
var distribution: Array = []
|
||||
var power: float = 1.0
|
||||
while power < value:
|
||||
var power: Variant = 1
|
||||
while E.lt(power, value):
|
||||
distribution.append(power)
|
||||
if distribution.size() > 4: distribution.pop_front()
|
||||
power *= 10.0
|
||||
if E.gt(distribution.size(), 4): distribution.pop_front()
|
||||
power = E.multiply(power, 10)
|
||||
distribution.append(value)
|
||||
while distribution.size() < 5: distribution.append(0.0)
|
||||
while E.lt(distribution.size(), 5): distribution.append(0)
|
||||
return distribution
|
||||
|
||||
func _set_action_values(values: Array, displayed: Array) -> void:
|
||||
@@ -64,17 +53,17 @@ func _set_action_values(values: Array, displayed: Array) -> void:
|
||||
action_values[ind] = values[ind]
|
||||
# Keep the original request for execution; show the exact planned output.
|
||||
var text: String = _format_action_value(displayed[ind])
|
||||
if action_labels[ind].text != text:
|
||||
if E.ne(action_labels[ind].text, text):
|
||||
action_labels[ind].text = text
|
||||
action_labels[ind].get_parent().get_node("TextureButton").disabled = values[ind] <= 0.0
|
||||
action_labels[ind].get_parent().get_node("TextureButton").disabled = E.le(values[ind], 0)
|
||||
|
||||
|
||||
func _format_action_value(value: float) -> String:
|
||||
if value < 1.0 or not is_finite(value) or value != floorf(value):
|
||||
func _format_action_value(value: Variant) -> String:
|
||||
if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))):
|
||||
return ""
|
||||
return Global.format_number(value)
|
||||
|
||||
|
||||
func _on_action_button_pressed(button_id: int) -> void:
|
||||
if action_values.get(button_id, 0.0) > 0.0:
|
||||
if E.gt(action_values.get(button_id, 0), 0):
|
||||
action_pressed.emit(page_id, action_values[button_id])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
var energy_bars: Array = []
|
||||
@@ -32,21 +33,21 @@ func _ignore_mouse_on_children(node: Node) -> void:
|
||||
|
||||
func update(world) -> void:
|
||||
var source: Dictionary = world.state
|
||||
var energy: float = clampf(source.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0)
|
||||
var energy: float = E.approx(E.clamp_value(E.divide(source.energy, Parameters.INITIAL_ENERGY), 0, 1))
|
||||
for bar in energy_bars: bar.scale.x = energy
|
||||
var density_value: float = world.environment.stellar_environment(source).density
|
||||
var density_value = world.environment.stellar_environment(source).density
|
||||
var density: Dictionary = Parameters.density_display(density_value)
|
||||
warning_border.self_modulate = Parameters.density_warning_color(density_value)
|
||||
for indicator in density_indicators:
|
||||
for mode in ["Density", "DensityRip", "DensityCrunch"]:
|
||||
indicator.get_node(mode).visible = mode == density.mode
|
||||
var pointer: Control = indicator.get_node("Pointer")
|
||||
pointer.position.x = 16.0 + 288.0 * density.position - pointer.size.x / 2.0
|
||||
pointer.position.x = 16.0 + 288.0 * E.approx(density.position) - pointer.size.x / 2.0
|
||||
pointer.self_modulate = Color.BLACK if density.mode == "DensityCrunch" else Color.WHITE
|
||||
for key in pointers:
|
||||
var pointer: Control = pointers[key]
|
||||
pointer.position.x = 16.0 * clampf(Parameters.value(source, key), 1.0, 19.0) - pointer.size.x / 2.0
|
||||
pointer.position.x = 16.0 * E.approx(E.clamp_value(Parameters.value(source, key), 1, 19)) - pointer.size.x / 2.0
|
||||
for i in trend_bars.size():
|
||||
var bar: Control = trend_bars[i]
|
||||
bar.pivot_offset = Vector2(0.0, bar.size.y)
|
||||
bar.scale.y = clampf(source.get("trends", {}).get(Parameters.TRENDS[i], 0.0), 0.0, 1.0)
|
||||
bar.scale.y = E.approx(E.clamp_value(source.get("trends", {}).get(Parameters.TRENDS[i], 0), 0, 1))
|
||||
|
||||
@@ -19,7 +19,7 @@ var item_sub_page: int = -1
|
||||
var item_type = ""
|
||||
|
||||
|
||||
func set_label(value: float) -> void:
|
||||
func set_label(value) -> void:
|
||||
var text: String = Global.format_number(value)
|
||||
if description.text != text:
|
||||
description.text = text
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
|
||||
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
|
||||
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Сохранены прежние точности округления: 1e-12 для выпуска и 1e-10 для событий цивилизаций.
|
||||
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Старые поправки `snappedf` удалены: количества считаются целочисленно, дробные вычисления выполняет [модуль точной арифметики](math/README.md). Там же описаны границы точности и формат сохранения 3.
|
||||
|
||||
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
|
||||
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func yearly_products(entity: OwniverseEntity) -> Dictionary:
|
||||
var products: Dictionary = {}
|
||||
if not entity.life_product.is_empty() and entity.lifespan > 0.0:
|
||||
if ((not entity.life_product.is_empty()) and E.gt(entity.lifespan, 0)):
|
||||
for resource in entity.life_product:
|
||||
products[resource] = maxf(0.0, entity.life_product[resource]) / entity.lifespan
|
||||
elif entity.product_per_year_type != "":
|
||||
products[entity.product_per_year_type] = maxf(0.0, entity.product_per_year_amount)
|
||||
products[resource] = E.divide(E.maximum(0, entity.life_product[resource]), entity.lifespan)
|
||||
elif E.ne(entity.product_per_year_type, ""):
|
||||
products[entity.product_per_year_type] = E.maximum(0, entity.product_per_year_amount)
|
||||
return products
|
||||
|
||||
|
||||
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary,
|
||||
distribution: Dictionary, from_year: float, to_year: float) -> void:
|
||||
func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Dictionary, distribution: Dictionary, from_year: Variant, to_year: Variant) -> void:
|
||||
var basic: Dictionary = {}
|
||||
var automatic: Dictionary = {}
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
var object_years: float = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||
var auto_share: float = clampf(entity.auto, 0.0, 1.0)
|
||||
var object_years: Variant = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||
var auto_share: Variant = E.clamp_value(entity.auto, 0, 1)
|
||||
var products: Dictionary = yearly_products(entity)
|
||||
for product in products:
|
||||
var amount: float = object_years * products[product] * WorldParameters.production(previous, entity)
|
||||
basic[product] = basic.get(product, 0.0) + amount * (1.0 - auto_share)
|
||||
automatic[product] = automatic.get(product, 0.0) + amount * auto_share
|
||||
var amount: Variant = E.multiply(E.multiply(object_years, products[product]), WorldParameters.production(previous, entity))
|
||||
basic[product] = E.add(basic.get(product, 0), E.multiply(amount, (E.subtract(1, auto_share))))
|
||||
automatic[product] = E.add(automatic.get(product, 0), E.multiply(amount, auto_share))
|
||||
# Basic and automatic output each retain their unproduced remainder.
|
||||
if not target.has("resource_remainders"):
|
||||
if (not target.has("resource_remainders")):
|
||||
target.resource_remainders = {"basic": {}, "automatic": {}}
|
||||
var pending: Dictionary = target.resource_remainders
|
||||
var requests: Dictionary = {}
|
||||
for id in basic:
|
||||
pending.basic[id] = pending.basic.get(id, 0.0) + basic[id]
|
||||
pending.automatic[id] = pending.automatic.get(id, 0.0) + automatic[id]
|
||||
pending.basic[id] = E.add(pending.basic.get(id, 0), basic[id])
|
||||
pending.automatic[id] = E.add(pending.automatic.get(id, 0), automatic[id])
|
||||
var totals: Dictionary = {}
|
||||
for channel in ["basic", "automatic"]:
|
||||
for id in pending[channel]:
|
||||
totals[id] = maxf(0.0, pending.basic.get(id, 0.0) + pending.automatic.get(id, 0.0))
|
||||
totals[id] = E.maximum(0, E.add(pending.basic.get(id, 0), pending.automatic.get(id, 0)))
|
||||
for id in totals:
|
||||
if id == "S" or id == "H":
|
||||
requests[id] = floorf(totals[id])
|
||||
var funded: Dictionary = QuantityMath.allocate_whole(requests, floorf(maxf(0.0, previous.energy)))
|
||||
if (E.eq(id, "S") or E.eq(id, "H")):
|
||||
requests[id] = E.floor_value(totals[id])
|
||||
var funded: Dictionary = QuantityMath.allocate_whole(requests, E.floor_value(E.maximum(0, previous.energy)))
|
||||
automatic = {}
|
||||
target.energy = previous.energy
|
||||
for id in totals:
|
||||
var amount: float = floorf(totals[id])
|
||||
if id == "S" or id == "H":
|
||||
amount = funded.get(id, 0.0)
|
||||
target.energy -= amount
|
||||
var amount: Variant = E.floor_value(totals[id])
|
||||
if (E.eq(id, "S") or E.eq(id, "H")):
|
||||
amount = funded.get(id, 0)
|
||||
target.energy = E.subtract(target.energy, amount)
|
||||
# Split the actual whole output, retaining the allocation error for next time.
|
||||
var auto_ratio: float = clampf(pending.automatic.get(id, 0.0) / totals[id], 0.0, 1.0) if totals[id] > 0.0 else 0.0
|
||||
var auto_amount: float = floorf(amount * auto_ratio)
|
||||
pending.automatic[id] = pending.automatic.get(id, 0.0) - auto_amount
|
||||
pending.basic[id] = pending.basic.get(id, 0.0) - (amount - auto_amount)
|
||||
var auto_ratio: Variant = (E.clamp_value(E.divide(pending.automatic.get(id, 0), totals[id]), 0, 1) if E.gt(totals[id], 0) else 0)
|
||||
var auto_amount: Variant = E.floor_value(E.multiply(amount, auto_ratio))
|
||||
pending.automatic[id] = E.subtract(pending.automatic.get(id, 0), auto_amount)
|
||||
pending.basic[id] = E.subtract(pending.basic.get(id, 0), (E.subtract(amount, auto_amount)))
|
||||
automatic[id] = auto_amount
|
||||
amount = world.ledger.credit(target, id, amount)
|
||||
world.batches.append_batch(target, id, amount, from_year, to_year)
|
||||
@@ -60,22 +60,22 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
# Keep unused automatic resources across cycles; they are part of free amounts.
|
||||
var budget: Dictionary = previous.get("auto_resources", {}).duplicate(true)
|
||||
for id in automatic:
|
||||
budget[id] = budget.get(id, 0.0) + automatic[id]
|
||||
budget[id] = E.add(budget.get(id, 0), automatic[id])
|
||||
for id in budget:
|
||||
budget[id] = minf(maxf(0.0, budget[id]), world.ledger.available(target, id))
|
||||
budget[id] = E.minimum(E.maximum(0, budget[id]), world.ledger.available(target, id))
|
||||
target.auto_resources = budget
|
||||
produce_brown_dwarfs(target, automatic.get("H", 0.0), from_year, to_year)
|
||||
produce_brown_dwarfs(target, automatic.get("H", 0), from_year, to_year)
|
||||
budget = ordinary_star_budget(target)
|
||||
|
||||
# One common multiplier preserves the initial count ratios, even under scarcity.
|
||||
var candidates: Dictionary = {}
|
||||
for id in distribution:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if entity.group_as != "Star":
|
||||
if E.ne(entity.group_as, "Star"):
|
||||
continue
|
||||
var has_cost: bool = false
|
||||
for resource in entity.cost:
|
||||
if entity.cost[resource] > 0.0:
|
||||
if E.gt(entity.cost[resource], 0):
|
||||
has_cost = true
|
||||
if has_cost:
|
||||
candidates[id] = distribution[id]
|
||||
@@ -85,88 +85,87 @@ func produce_for_period(previous: Dictionary, target: Dictionary, quantities: Di
|
||||
var costs: Dictionary = {}
|
||||
var needs: Dictionary = {}
|
||||
for id in candidates:
|
||||
if auto_capacity(target, world.catalog.entities[id], budget) <= 0.0:
|
||||
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 0):
|
||||
continue
|
||||
for resource in world.catalog.entities[id].cost:
|
||||
var cost: float = maxf(0.0, world.catalog.entities[id].cost[resource]) * candidates[id]
|
||||
costs[resource] = costs.get(resource, 0.0) + cost
|
||||
needs[resource] = needs.get(resource, 0.0) + cost
|
||||
var cost: Variant = E.multiply(E.maximum(0, world.catalog.entities[id].cost[resource]), candidates[id])
|
||||
costs[resource] = E.add(costs.get(resource, 0), cost)
|
||||
needs[resource] = E.add(needs.get(resource, 0), cost)
|
||||
for resource in world.catalog.entities[id].bind:
|
||||
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, world.catalog.entities[id].bind[resource]) * candidates[id]
|
||||
var scale: float = INF
|
||||
needs[resource] = E.add(needs.get(resource, 0), E.multiply(E.maximum(0, world.catalog.entities[id].bind[resource]), candidates[id]))
|
||||
var scale: Variant = INF
|
||||
for resource in costs:
|
||||
if costs[resource] > 0.0:
|
||||
scale = minf(scale, budget.get(resource, 0.0) / costs[resource])
|
||||
if E.gt(costs[resource], 0):
|
||||
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
|
||||
for resource in needs:
|
||||
if needs[resource] > 0.0:
|
||||
scale = minf(scale, world.ledger.available(target, resource) / needs[resource])
|
||||
if E.gt(needs[resource], 0):
|
||||
scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), needs[resource]))
|
||||
var concrete_needs: Dictionary = world.ledger.expand_requirements(target, needs)
|
||||
for resource in concrete_needs:
|
||||
if concrete_needs[resource] > 0.0:
|
||||
scale = minf(scale, world.ledger.available(target, resource) / concrete_needs[resource])
|
||||
if not is_finite(scale) or scale <= 0.0:
|
||||
if E.gt(concrete_needs[resource], 0):
|
||||
scale = E.minimum(scale, E.divide(world.ledger.available(target, resource), concrete_needs[resource]))
|
||||
if ((not E.finite(scale)) or E.le(scale, 0)):
|
||||
return
|
||||
# Rare types are served first; signed count deficits compensate later cycles.
|
||||
if not target.has("auto_star_deficits"):
|
||||
if (not target.has("auto_star_deficits")):
|
||||
target.auto_star_deficits = {}
|
||||
var deficits: Dictionary = target.auto_star_deficits
|
||||
var order: Array = candidates.keys()
|
||||
order.sort_custom(func(a: String, b: String) -> bool:
|
||||
return candidates[a] < candidates[b] if candidates[a] != candidates[b] else a < b)
|
||||
order.sort_custom(func ( a : String , b : String ) -> bool : return (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
|
||||
var created: Dictionary = {}
|
||||
var metallicity: float = world.environment.stellar_environment(target).metallicity
|
||||
var total_created: float = 0.0
|
||||
var metallicity: Variant = world.environment.stellar_environment(target).metallicity
|
||||
var total_created: Variant = 0
|
||||
for id in order:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
var step: float = world.production.production_step(entity)
|
||||
if not is_finite(step):
|
||||
var step: Variant = world.production.production_step(entity)
|
||||
if (not E.finite(step)):
|
||||
continue
|
||||
var quota: float = maxf(0.0, candidates[id] * scale + deficits.get(id, 0.0))
|
||||
var desired: float = floorf(quota / step) * step
|
||||
var actual: float = minf(desired, auto_capacity(target, entity, budget))
|
||||
if actual <= 0.0:
|
||||
var quota: Variant = E.maximum(0, E.add(E.multiply(candidates[id], scale), deficits.get(id, 0)))
|
||||
var desired: Variant = E.multiply(E.floor_value(E.divide(quota, step)), step)
|
||||
var actual: Variant = E.minimum(desired, auto_capacity(target, entity, budget))
|
||||
if E.le(actual, 0):
|
||||
continue
|
||||
if not world.production.try_production(target, entity, actual, true):
|
||||
if (not world.production.try_production(target, entity, actual, true)):
|
||||
continue
|
||||
budget = ordinary_star_budget(target)
|
||||
created[id] = actual
|
||||
total_created += actual
|
||||
total_created = E.add(total_created, actual)
|
||||
for id in candidates:
|
||||
# Credit only actual production, so retained resources aren't counted twice.
|
||||
deficits[id] = deficits.get(id, 0.0) + candidates[id] * total_created - created.get(id, 0.0)
|
||||
deficits[id] = E.subtract(E.add(deficits.get(id, 0), E.multiply(candidates[id], total_created)), created.get(id, 0))
|
||||
for id in created:
|
||||
world.ledger.credit(target, id, created[id])
|
||||
world.batches.append_batch(target, id, created[id], from_year, to_year)
|
||||
world.stellar.create_planets(target, id, created[id], from_year, to_year, metallicity)
|
||||
|
||||
|
||||
func produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: float, to_year: float) -> void:
|
||||
if not world.catalog.entities.has("HBD"):
|
||||
func produce_brown_dwarfs(target: Dictionary, new_hydrogen: Variant, from_year: Variant, to_year: Variant) -> void:
|
||||
if (not world.catalog.entities.has("HBD")):
|
||||
return
|
||||
var entity: OwniverseEntity = world.catalog.entities.HBD
|
||||
var cost: float = entity.cost.get("H", 0.0)
|
||||
if cost <= 0.0:
|
||||
var cost: Variant = entity.cost.get("H", 0)
|
||||
if E.le(cost, 0):
|
||||
return
|
||||
var quota: float = target.auto_star_deficits.get("HBD", 0.0) + maxf(0.0, new_hydrogen) * 0.001 / cost
|
||||
var fuel: float = maxf(0.0, target.auto_star_deficits.get("HBD_fuel", 0.0))
|
||||
var available: float = minf(target.auto_resources.get("H", 0.0), world.ledger.available(target, "H"))
|
||||
quota = minf(quota, (available + fuel) / cost)
|
||||
var expected: float = maxf(0.0, snappedf(quota, 1e-9))
|
||||
var amount: float = floorf(expected)
|
||||
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.divide(E.multiply(E.maximum(0, new_hydrogen), E.of("0.001")), cost))
|
||||
var fuel: Variant = E.maximum(0, target.auto_star_deficits.get("HBD_fuel", 0))
|
||||
var available: Variant = E.minimum(target.auto_resources.get("H", 0), world.ledger.available(target, "H"))
|
||||
quota = E.minimum(quota, E.divide((E.add(available, fuel)), cost))
|
||||
var expected: Variant = E.maximum(0, quota)
|
||||
var amount: Variant = E.floor_value(expected)
|
||||
# Do not retry a fractional chance on frames with no new automatic fuel.
|
||||
if new_hydrogen > 0.0 and expected > amount:
|
||||
var random_state: float = target.auto_star_deficits.get("HBD_random", 1234567.0)
|
||||
random_state = fmod(random_state * 48271.0, 2147483647.0)
|
||||
if (E.gt(new_hydrogen, 0) and E.gt(expected, amount)):
|
||||
var random_state: Variant = target.auto_star_deficits.get("HBD_random", 1234567)
|
||||
random_state = E.remainder(E.multiply(random_state, 48271), E.of("2147483647.0"))
|
||||
target.auto_star_deficits.HBD_random = random_state
|
||||
if random_state / 2147483647.0 < expected - amount:
|
||||
amount += 1.0
|
||||
amount = minf(amount, floorf(snappedf((available + fuel) / cost, 1e-9)))
|
||||
var charge: float = ceilf(maxf(0.0, snappedf(amount * cost - fuel, 1e-9)))
|
||||
if amount > 0.0 and charge <= available:
|
||||
if E.lt(E.divide(random_state, E.of("2147483647.0")), E.subtract(expected, amount)):
|
||||
amount = E.add(amount, 1)
|
||||
amount = E.minimum(amount, E.floor_value(E.divide((E.add(available, fuel)), cost)))
|
||||
var charge: Variant = E.ceil_value(E.maximum(0, E.subtract(E.multiply(amount, cost), fuel)))
|
||||
if (E.gt(amount, 0) and E.le(charge, available)):
|
||||
world.ledger.take_available(target, "H", charge)
|
||||
target.auto_resources.H = maxf(0.0, target.auto_resources.get("H", 0.0) - charge)
|
||||
target.auto_star_deficits.HBD_fuel = maxf(0.0, snappedf(fuel + charge - amount * cost, 1e-9))
|
||||
quota -= amount
|
||||
target.auto_resources.H = E.maximum(0, E.subtract(target.auto_resources.get("H", 0), charge))
|
||||
target.auto_star_deficits.HBD_fuel = E.maximum(0, E.subtract(E.add(fuel, charge), E.multiply(amount, cost)))
|
||||
quota = E.subtract(quota, amount)
|
||||
world.ledger.credit(target, "HBD", amount)
|
||||
world.batches.append_batch(target, "HBD", amount, from_year, to_year)
|
||||
target.auto_star_deficits.HBD = quota
|
||||
@@ -175,52 +174,50 @@ func produce_brown_dwarfs(target: Dictionary, new_hydrogen: float, from_year: fl
|
||||
func ordinary_star_budget(target: Dictionary) -> Dictionary:
|
||||
var budget: Dictionary = target.auto_resources.duplicate()
|
||||
if world.catalog.entities.has("HBD"):
|
||||
var reserved: float = maxf(0.0, target.auto_star_deficits.get("HBD", 0.0) * world.catalog.entities.HBD.cost.get("H", 0.0) - target.auto_star_deficits.get("HBD_fuel", 0.0))
|
||||
budget.H = maxf(0.0, budget.get("H", 0.0) - reserved)
|
||||
var reserved: Variant = E.maximum(0, E.subtract(E.multiply(target.auto_star_deficits.get("HBD", 0), world.catalog.entities.HBD.cost.get("H", 0)), target.auto_star_deficits.get("HBD_fuel", 0)))
|
||||
budget.H = E.maximum(0, E.subtract(budget.get("H", 0), reserved))
|
||||
return budget
|
||||
|
||||
|
||||
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> float:
|
||||
var capacity: float = INF
|
||||
func auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Dictionary) -> Variant:
|
||||
var capacity: Variant = INF
|
||||
var needs: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
var cost: float = maxf(0.0, entity.cost[resource])
|
||||
var cost: Variant = E.maximum(0, entity.cost[resource])
|
||||
needs[resource] = cost
|
||||
if cost > 0.0:
|
||||
capacity = minf(capacity, floorf(budget.get(resource, 0.0) / cost))
|
||||
if E.gt(cost, 0):
|
||||
capacity = E.minimum(capacity, E.floor_value(E.divide(budget.get(resource, 0), cost)))
|
||||
for resource in entity.bind:
|
||||
needs[resource] = needs.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||
needs[resource] = E.add(needs.get(resource, 0), E.maximum(0, entity.bind[resource]))
|
||||
for resource in needs:
|
||||
if needs[resource] > 0.0:
|
||||
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / needs[resource]))
|
||||
if E.gt(needs[resource], 0):
|
||||
capacity = E.minimum(capacity, E.floor_value(E.divide(world.ledger.available(source, resource), needs[resource])))
|
||||
var concrete: Dictionary = world.ledger.expand_requirements(source, needs)
|
||||
for resource in concrete:
|
||||
if concrete[resource] > 0.0:
|
||||
capacity = minf(capacity, floorf(world.ledger.available(source, resource) / concrete[resource]))
|
||||
var step: float = world.production.production_step(entity)
|
||||
return floorf(capacity / step) * step if is_finite(capacity) and is_finite(step) else 0.0
|
||||
if E.gt(concrete[resource], 0):
|
||||
capacity = E.minimum(capacity, E.floor_value(E.divide(world.ledger.available(source, resource), concrete[resource])))
|
||||
var step: Variant = world.production.production_step(entity)
|
||||
return (E.multiply(E.floor_value(E.divide(capacity, step)), step) if (E.finite(capacity) and E.finite(step)) else 0)
|
||||
|
||||
|
||||
func produce_newborn_lifetime(target: Dictionary, id: String, count: float,
|
||||
born_from: float, born_to: float, to_year: float, before_batches: Array,
|
||||
remaining_object_years: float = NAN) -> void:
|
||||
func produce_newborn_lifetime(target: Dictionary, id: String, count: Variant, born_from: Variant, born_to: Variant, to_year: Variant, before_batches: Array, remaining_object_years: Variant = NAN) -> void:
|
||||
var products: Dictionary = yearly_products(world.catalog.entities[id])
|
||||
if products.is_empty() or count <= 0.0:
|
||||
if (products.is_empty() or E.le(count, 0)):
|
||||
return
|
||||
var object_years: float = remaining_object_years if is_finite(remaining_object_years) else count * maxf(0.0, to_year - (born_from + born_to) * 0.5)
|
||||
var object_years: Variant = (remaining_object_years if E.finite(remaining_object_years) else E.multiply(count, E.maximum(0, E.subtract(to_year, E.multiply((E.add(born_from, born_to)), E.of("0.5"))))))
|
||||
var previous_amounts: Dictionary = {}
|
||||
for old : Dictionary in before_batches:
|
||||
var start: float = old.get("created_from", -INF)
|
||||
if not previous_amounts.has(start):
|
||||
previous_amounts[start] = old.amount
|
||||
var start: Variant = old.get("created_from", E.neg(INF))
|
||||
if (not previous_amounts.has(str(start))):
|
||||
previous_amounts[str(start)] = old.amount
|
||||
for batch : Dictionary in target.batches.get(id, []):
|
||||
var added: float = batch.amount - previous_amounts.get(batch.get("created_from", INF), 0.0)
|
||||
object_years -= maxf(0.0, added) * world.batches.expired_years(to_year, batch.start_year, batch.end_year)
|
||||
object_years = maxf(0.0, object_years)
|
||||
var added: Variant = E.subtract(batch.amount, previous_amounts.get(str(batch.get("created_from", INF)), 0))
|
||||
object_years = E.subtract(object_years, E.multiply(E.maximum(0, added), world.batches.expired_years(to_year, batch.start_year, batch.end_year)))
|
||||
object_years = E.maximum(0, object_years)
|
||||
for resource in products:
|
||||
var output: float = object_years * products[resource] * WorldParameters.production(target, world.catalog.entities[id])
|
||||
if resource in ["H", "S"]:
|
||||
output = minf(output, target.energy)
|
||||
var actual: float = world.ledger.credit(target, resource, output)
|
||||
if resource in ["H", "S"]:
|
||||
target.energy = maxf(0.0, target.energy - actual)
|
||||
var output: Variant = E.multiply(E.multiply(object_years, products[resource]), WorldParameters.production(target, world.catalog.entities[id]))
|
||||
if E.contains(["H", "S"], resource):
|
||||
output = E.minimum(output, target.energy)
|
||||
var actual: Variant = world.ledger.credit(target, resource, output)
|
||||
if E.contains(["H", "S"], resource):
|
||||
target.energy = E.maximum(0, E.subtract(target.energy, actual))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
@@ -7,43 +8,42 @@ 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:
|
||||
static func total(pop: Dictionary) -> Variant:
|
||||
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]
|
||||
for key in from: to[key] = E.add(to.get(key, 0), from[key])
|
||||
|
||||
static func take(world, pop: Dictionary, count: float) -> Dictionary:
|
||||
static func take(world, pop: Dictionary, count: Variant) -> Dictionary:
|
||||
var taken: Dictionary = QuantityMath.allocate_whole(pop, count)
|
||||
for key in taken: pop[key] -= taken[key]
|
||||
for key in taken: pop[key] = E.subtract(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 = {}
|
||||
static func rate(target: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
|
||||
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
|
||||
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:
|
||||
if count <= 0.0 or QuantityMath.sum(weights) <= 0.0: return {}
|
||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
||||
static func split(target: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||
if (E.le(count, 0) or E.le(QuantityMath.sum(weights), 0)): return {}
|
||||
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
|
||||
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:
|
||||
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary, diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
||||
var weights: Dictionary = {}
|
||||
for ethic in pop:
|
||||
var influence: float = float(rows[ethic].diplomacy_rate) if diplomacy else 1.0
|
||||
var influence: Variant = (E.of(rows[ethic].diplomacy_rate) if diplomacy else 1)
|
||||
for other in ETHICS:
|
||||
var weight: float = 9.0 if colony else 1.0
|
||||
if other == ethic: weight = 960.0 if colony else 196.0
|
||||
elif other == OPPOSITE[ethic]: weight = 4.0 if colony else 0.0
|
||||
weights[other] = weights.get(other, 0.0) + pop[ethic] * influence * weight
|
||||
if total(weights) <= 0.0: return pop.duplicate()
|
||||
var weight: Variant = (9 if colony else 1)
|
||||
if E.eq(other, ethic): weight = (960 if colony else 196)
|
||||
elif E.eq(other, OPPOSITE[ethic]): weight = (4 if colony else 0)
|
||||
weights[other] = E.add(weights.get(other, 0), E.multiply(E.multiply(pop[ethic], influence), weight))
|
||||
if E.le(total(weights), 0): return pop.duplicate()
|
||||
return split(target, key, total(pop), weights)
|
||||
|
||||
static func emit(plan: Dictionary, destination: String, pop: Dictionary) -> void:
|
||||
if not plan.outputs.has(destination): plan.outputs[destination] = {}
|
||||
if (not plan.outputs.has(destination)): plan.outputs[destination] = {}
|
||||
add(plan.outputs[destination], pop)
|
||||
|
||||
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
|
||||
@@ -51,51 +51,50 @@ static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: D
|
||||
var weights: Dictionary = {}
|
||||
for term in str(rules[plan.id].ethics[ethic].fall).split("^", false):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
weights[pair[0]] = float(pair[1])
|
||||
if weights.is_empty(): weights[rules[plan.id].previous] = 1.0
|
||||
var shares: Dictionary = split(target, "fall:" + plan.id + ":" + ethic, pop[ethic], weights)
|
||||
weights[pair[0]] = E.of(pair[1])
|
||||
if weights.is_empty(): weights[rules[plan.id].previous] = 1
|
||||
var shares: Dictionary = split(target, E.add(E.add(E.add("fall:", plan.id), ":"), ethic), pop[ethic], weights)
|
||||
for stage in shares: emit(plan, stage, {ethic: shares[stage]})
|
||||
|
||||
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary,
|
||||
plan: Dictionary, pop: Dictionary) -> Dictionary:
|
||||
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary, plan: Dictionary, pop: Dictionary) -> Dictionary:
|
||||
var successor: String = rules[plan.id].next
|
||||
var admitted: float = State.admit_transition(budget, rules, plan.id, successor, total(pop))
|
||||
var admitted: Variant = State.admit_transition(budget, rules, plan.id, successor, total(pop))
|
||||
var accepted: Dictionary = take(world, pop, admitted)
|
||||
emit(plan, successor, accepted)
|
||||
return pop
|
||||
|
||||
static func new_plan(id: String, year: float) -> Dictionary:
|
||||
return {"id": id, "from": year, "to": -INF, "amount": 0.0, "outputs": {}}
|
||||
static func new_plan(id: String, year: Variant) -> Dictionary:
|
||||
return {"id": id, "from": year, "to": E.neg(INF), "amount": 0, "outputs": {}}
|
||||
|
||||
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: float, year: float) -> Dictionary:
|
||||
var old_total: float = total(pop)
|
||||
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: Variant, year: Variant) -> Dictionary:
|
||||
var old_total: Variant = total(pop)
|
||||
var removed: Dictionary = take(world, pop, count)
|
||||
var actual: float = total(removed)
|
||||
if actual > 0.0:
|
||||
world.batches.scale_batches(target, plan.id, (old_total - actual) / old_total)
|
||||
plan.amount += actual
|
||||
plan.to = maxf(plan.to, year)
|
||||
var actual: Variant = total(removed)
|
||||
if E.gt(actual, 0):
|
||||
world.batches.scale_batches(target, plan.id, E.divide((E.subtract(old_total, actual)), old_total))
|
||||
plan.amount = E.add(plan.amount, actual)
|
||||
plan.to = E.maximum(plan.to, year)
|
||||
return removed
|
||||
|
||||
static func prepare(world, target: Dictionary, expired: Dictionary, year: float, budget: Dictionary, rules: Dictionary) -> Dictionary:
|
||||
static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary) -> Dictionary:
|
||||
State.reconcile(world, target, rules)
|
||||
var remaining: Dictionary = target.ethics.duplicate(true)
|
||||
var plans: Dictionary = {}
|
||||
var colonies: Array = []
|
||||
var collateral: Dictionary = {}
|
||||
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
|
||||
var order: float = WorldParameters.value(target, "Order")
|
||||
if not target.has("civilization_stats"): target.civilization_stats = {}
|
||||
var order: Variant = WorldParameters.value(target, "Order")
|
||||
if (not target.has("civilization_stats")): target.civilization_stats = {}
|
||||
var ids: Array = expired.keys()
|
||||
ids.sort()
|
||||
for id in ids:
|
||||
if not rules.has(id) or expired[id].is_empty(): continue
|
||||
if ((not rules.has(id)) or expired[id].is_empty()): continue
|
||||
var plan: Dictionary = new_plan(id, year)
|
||||
plans[id] = plan
|
||||
for part : Dictionary in expired[id]:
|
||||
plan.amount += part.amount
|
||||
plan.from = minf(plan.from, part.from)
|
||||
plan.to = maxf(plan.to, part.to)
|
||||
plan.amount = E.add(plan.amount, part.amount)
|
||||
plan.from = E.minimum(plan.from, part.from)
|
||||
plan.to = E.maximum(plan.to, part.to)
|
||||
var triggered: Dictionary = take(world, remaining[id], plan.amount)
|
||||
var successes: Dictionary = {}
|
||||
var failures: Dictionary = {}
|
||||
@@ -103,54 +102,54 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
var settlers: Dictionary = {}
|
||||
for ethic in triggered:
|
||||
var row: Dictionary = rules[id].ethics[ethic]
|
||||
var p: float = maxf(0.0, float(row.progress) * modifiers.progress)
|
||||
var d: float = maxf(0.0, float(row.degradation) * modifiers.degradation)
|
||||
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 p: Variant = E.maximum(0, E.multiply(E.of(row.progress), modifiers.progress))
|
||||
var d: Variant = E.maximum(0, E.multiply(E.of(row.degradation), modifiers.degradation))
|
||||
var scale: Variant = E.maximum(1, E.add(p, d))
|
||||
var pro: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":pro"), triggered[ethic], E.divide(p, scale))
|
||||
var deg: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":deg"), triggered[ethic], E.divide(d, scale))
|
||||
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))
|
||||
if rules[id].fights: warriors[ethic] = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":war"), triggered[ethic], E.minimum(1, E.multiply(E.of(row.war), modifiers.war)))
|
||||
if rules[id].colonizes: settlers[ethic] = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":col"), triggered[ethic], E.minimum(1, E.multiply(E.of(row.colonization), modifiers.colonization)))
|
||||
successes[ethic] = bounded.pro
|
||||
failures[ethic] = bounded.deg
|
||||
triggered[ethic] -= bounded.pro + bounded.deg
|
||||
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
|
||||
add(triggered, progress(world, target, rules, budget, plan, successes))
|
||||
fall(target, plan, rules, failures)
|
||||
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))
|
||||
for ethic in warriors: warriors[ethic] = E.minimum(warriors[ethic], triggered[ethic])
|
||||
var wars: Variant = E.minimum(total(warriors), E.floor_value(E.divide((E.add(total(triggered), total(remaining[id]))), 2)))
|
||||
warriors = QuantityMath.allocate_whole(warriors, wars)
|
||||
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
|
||||
var loss_expected: float = outcomes.get("deg", 0.0) * 2.0 + outcomes.get("pro25", 0.0) + outcomes.get("pro50", 0.0) * 1.5 + outcomes.get("pro75", 0.0) * 2.0
|
||||
var winners: Dictionary = take(world, warriors, rate(target, id + ":wins", win_expected, 1.0))
|
||||
var attackers_lost: Dictionary = take(world, warriors, rate(target, id + ":attackers_lost", outcomes.get("deg", 0.0) / order, 1.0))
|
||||
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
|
||||
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 winners: Dictionary = take(world, warriors, rate(target, E.add(id, ":wins"), win_expected, 1))
|
||||
var attackers_lost: Dictionary = take(world, warriors, rate(target, E.add(id, ":attackers_lost"), E.divide(outcomes.get("deg", 0), order), 1))
|
||||
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 losses: Variant = E.minimum(rate(target, E.add(id, ":loss"), E.divide(loss_expected, order), 1), E.add(total(triggered), total(remaining[id])))
|
||||
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)
|
||||
add(triggered, warriors)
|
||||
var damage: float = rate(target, id + ":collateral", (outcomes.get("pro50", 0.0) * 10.0 + outcomes.get("pro75", 0.0) * 30.0) / order, 1.0)
|
||||
collateral[rules[id].previous] = collateral.get(rules[id].previous, 0.0) + damage
|
||||
target.civilization_stats.wars = target.civilization_stats.get("wars", 0.0) + wars
|
||||
var damage: Variant = rate(target, E.add(id, ":collateral"), E.divide((E.add(E.multiply(outcomes.get("pro50", 0), 10), E.multiply(outcomes.get("pro75", 0), 30))), order), 1)
|
||||
collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage)
|
||||
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars)
|
||||
emit(plan, id, triggered)
|
||||
if total(settlers) > 0.0: colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
|
||||
if E.gt(total(settlers), 0): colonies.append({"stage": rules[id].colony, "source": id, "population": settlers})
|
||||
# Collateral is applied after all stages planned their own attempts.
|
||||
for id in collateral:
|
||||
if not rules.has(id): continue
|
||||
if not plans.has(id): plans[id] = new_plan(id, year)
|
||||
if (not rules.has(id)): continue
|
||||
if (not plans.has(id)): plans[id] = new_plan(id, year)
|
||||
var plan: Dictionary = plans[id]
|
||||
var retry: Dictionary = plan.outputs.get(id, {})
|
||||
var amount: float = minf(collateral[id], total(retry) + total(remaining[id]))
|
||||
var amount: Variant = E.minimum(collateral[id], E.add(total(retry), total(remaining[id])))
|
||||
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)
|
||||
target.civilization_stats.collateral = target.civilization_stats.get("collateral", 0.0) + total(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:
|
||||
@@ -159,70 +158,70 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
||||
for destination in plan.outputs:
|
||||
var pop: Dictionary = plan.outputs[destination]
|
||||
if rules.has(destination):
|
||||
if destination != "Ct":
|
||||
pop = drift(target, id + ":" + destination + ":drift", pop, rules[id].ethics, destination == id and rules[id].diplomacy)
|
||||
if not incoming.has(destination): incoming[destination] = {}
|
||||
if E.ne(destination, "Ct"):
|
||||
pop = drift(target, E.add(E.add(E.add(id, ":"), destination), ":drift"), pop, rules[id].ethics, (E.eq(destination, id) and rules[id].diplomacy))
|
||||
if (not incoming.has(destination)): incoming[destination] = {}
|
||||
add(incoming[destination], pop)
|
||||
products[destination] = total(pop)
|
||||
if State.is_space_civilization(id, rules) and world.catalog.entities[destination].group_as == "Planet":
|
||||
planetary[destination] = planetary.get(destination, 0.0) + total(pop)
|
||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": maxf(plan.from, plan.to), "products": products}]
|
||||
if (State.is_space_civilization(id, rules) and E.eq(world.catalog.entities[destination].group_as, "Planet")):
|
||||
planetary[destination] = E.add(planetary.get(destination, 0), total(pop))
|
||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
|
||||
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
||||
|
||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: float, rules: Dictionary) -> void:
|
||||
static func finish(world, target: Dictionary, transaction: Dictionary, year: Variant, rules: Dictionary) -> void:
|
||||
target.ethics = transaction.remaining
|
||||
for id in transaction.incoming:
|
||||
if not target.ethics.has(id): target.ethics[id] = {}
|
||||
if (not target.ethics.has(id)): target.ethics[id] = {}
|
||||
add(target.ethics[id], transaction.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 = {}
|
||||
for id in physical:
|
||||
if world.catalog.entities[id].event_group == "DwStar" and physical[id] > 0.0: hosts[id] = physical[id]
|
||||
if (E.eq(world.catalog.entities[id].event_group, "DwStar") and E.gt(physical[id], 0)): hosts[id] = physical[id]
|
||||
for id in transaction.planetary:
|
||||
var shares: Dictionary = split(target, "return:" + id, minf(transaction.planetary[id], target.amount.get(id, 0.0)), hosts)
|
||||
var shares: Dictionary = split(target, E.add("return:", id), E.minimum(transaction.planetary[id], target.amount.get(id, 0)), hosts)
|
||||
for owner in shares:
|
||||
if not target.binds.has(owner): target.binds[owner] = {}
|
||||
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + shares[owner]
|
||||
target.amount[id] -= shares[owner]
|
||||
if (not target.binds.has(owner)): target.binds[owner] = {}
|
||||
target.binds[owner][id] = E.add(target.binds[owner].get(id, 0), shares[owner])
|
||||
target.amount[id] = E.subtract(target.amount[id], shares[owner])
|
||||
colonize(world, target, transaction.colonies, year, rules)
|
||||
State.reconcile(world, target, rules)
|
||||
var worship: float = 0.0
|
||||
var worship: Variant = 0
|
||||
for id in rules:
|
||||
if not rules[id].worships: continue
|
||||
for ethic in target.ethics[id]: worship += target.ethics[id][ethic] * float(rules[id].ethics[ethic].worship)
|
||||
if (not rules[id].worships): continue
|
||||
for ethic in target.ethics[id]: worship = E.add(worship, E.multiply(target.ethics[id][ethic], E.of(rules[id].ethics[ethic].worship)))
|
||||
target.civilization_stats.worship = worship
|
||||
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: float, rules: Dictionary) -> void:
|
||||
static func colonize(world, target: Dictionary, colonies: Array, year: Variant, rules: Dictionary) -> void:
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var stock: Dictionary = {}
|
||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0.0)
|
||||
for id in ["GG", "AB", "RP", "HP"]: stock[id] = physical.get(id, 0)
|
||||
var requests: Dictionary = {}
|
||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
||||
for i in colonies.size():
|
||||
var colony: Dictionary = colonies[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)
|
||||
population = drift(target, E.add(colony.source, ":colonies"), population, rules[colony.source].ethics, false, true)
|
||||
var count: Variant = total(population)
|
||||
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 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 taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||
world.batches.scale_batches(target, id, 1.0 - amount / stock[id])
|
||||
stock[id] -= 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 taken[owner] <= 0.0: continue
|
||||
if owner == "": target.amount[id] -= taken[owner]
|
||||
else: target.binds[owner][id] -= taken[owner]
|
||||
var created: float = world.ledger.credit(target, colony.stage, taken[owner])
|
||||
if owner != "":
|
||||
target.amount[colony.stage] -= created
|
||||
target.binds[owner][colony.stage] = target.binds[owner].get(colony.stage, 0.0) + created
|
||||
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] = {}
|
||||
if (not target.ethics.has(colony.stage)): target.ethics[colony.stage] = {}
|
||||
add(target.ethics[colony.stage], population)
|
||||
target.civilization_stats.colonies = target.civilization_stats.get("colonies", 0.0) + count
|
||||
target.civilization_stats.colonies = E.add(target.civilization_stats.get("colonies", 0), count)
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
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:
|
||||
return rules.has(id) and str(rules[id].home) != ""
|
||||
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.
|
||||
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
if not target.has("ethics"):
|
||||
if (not target.has("ethics")):
|
||||
target.ethics = {}
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
for id in rules:
|
||||
var count: float = physical.get(id, 0.0)
|
||||
var count: Variant = physical.get(id, 0)
|
||||
var current: Dictionary = target.ethics.get(id, {})
|
||||
var total: float = 0.0
|
||||
var total: Variant = 0
|
||||
for ethic in ETHICS:
|
||||
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
||||
total += current[ethic]
|
||||
if total > count:
|
||||
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 total < count:
|
||||
var missing: float = count - total
|
||||
var base: float = floorf(missing / ETHICS.size())
|
||||
var rest: int = int(missing - base * ETHICS.size())
|
||||
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]] += base + (1.0 if i < rest else 0.0)
|
||||
current[ETHICS[i]] = E.add(current[ETHICS[i]], E.add(base, ((1 if E.lt(i, rest) else 0))))
|
||||
target.ethics[id] = current
|
||||
|
||||
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
|
||||
for held : Dictionary in target.binds.values():
|
||||
for id in held:
|
||||
if is_space_civilization(id, rules):
|
||||
target.amount[id] = target.amount.get(id, 0.0) + held[id]
|
||||
held[id] = 0.0
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), held[id])
|
||||
held[id] = 0
|
||||
|
||||
# A shared budget per stellar family, including stars bound into structures.
|
||||
# Reservations affect capacity, not the physical stock or its production.
|
||||
@@ -45,65 +46,64 @@ static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
||||
for id in physical:
|
||||
var entity = world.catalog.entities[id]
|
||||
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
||||
if entity.event_group == home or entity.count_as == home or id == home:
|
||||
capacity[home] = capacity.get(home, 0.0) + physical[id]
|
||||
capacity["SMBH"] = maxf(0.0, target.amount.get("SMBH", 0.0))
|
||||
if ((E.eq(entity.event_group, home) or E.eq(entity.count_as, home)) or E.eq(id, home)):
|
||||
capacity[home] = E.add(capacity.get(home, 0), physical[id])
|
||||
capacity["SMBH"] = E.maximum(0, target.amount.get("SMBH", 0))
|
||||
for held : Dictionary in target.binds.values():
|
||||
capacity["SMBH"] += maxf(0.0, held.get("SMBH", 0.0))
|
||||
capacity["SMBH"] = E.add(capacity["SMBH"], E.maximum(0, held.get("SMBH", 0)))
|
||||
var populations: Dictionary = {}
|
||||
var requested: Dictionary = {}
|
||||
for id in rules:
|
||||
if not is_space_civilization(id, rules):
|
||||
if (not is_space_civilization(id, rules)):
|
||||
continue
|
||||
populations[id] = physical.get(id, 0.0)
|
||||
populations[id] = physical.get(id, 0)
|
||||
var home: String = rules[id].home
|
||||
requested[home] = requested.get(home, 0.0) + populations[id] * maxf(1.0, rules[id].stars)
|
||||
requested[home] = E.add(requested.get(home, 0), E.multiply(populations[id], E.maximum(1, rules[id].stars)))
|
||||
var free: Dictionary = {}
|
||||
for home in capacity:
|
||||
free[home] = maxf(0.0, capacity[home] - requested.get(home, 0.0))
|
||||
free[home] = E.maximum(0, E.subtract(capacity[home], requested.get(home, 0)))
|
||||
return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules)}
|
||||
|
||||
# Account for the source's existing reservation when changing stages. A failed
|
||||
# transition retains its population at the source, rather than losing objects.
|
||||
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String,
|
||||
destination: String, requested: float) -> float:
|
||||
if source == destination or not is_space_civilization(destination, rules):
|
||||
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String, destination: String, requested: Variant) -> Variant:
|
||||
if (E.eq(source, destination) or (not is_space_civilization(destination, rules))):
|
||||
return requested
|
||||
var home: String = rules[destination].home
|
||||
var cost: float = maxf(1.0, rules[destination].stars)
|
||||
var cost: Variant = E.maximum(1, rules[destination].stars)
|
||||
var old_home: String = ""
|
||||
var old_cost: float = 0.0
|
||||
var old_cost: Variant = 0
|
||||
if is_space_civilization(source, rules):
|
||||
old_home = rules[source].home
|
||||
old_cost = maxf(1.0, rules[source].stars)
|
||||
old_cost = E.maximum(1, rules[source].stars)
|
||||
# Existing overcapacity is handled by the separate population policy.
|
||||
if budget.requested.get(home, 0.0) > budget.capacity.get(home, 0.0):
|
||||
return 0.0
|
||||
var net_cost: float = cost - old_cost if old_home == home else cost
|
||||
var admitted: float = floorf(maxf(0.0, requested))
|
||||
if net_cost > 0.0:
|
||||
admitted = minf(admitted, floorf(budget.free.get(home, 0.0) / net_cost))
|
||||
if E.gt(budget.requested.get(home, 0), budget.capacity.get(home, 0)):
|
||||
return 0
|
||||
var net_cost: Variant = (E.subtract(cost, old_cost) if E.eq(old_home, home) else cost)
|
||||
var admitted: Variant = E.floor_value(E.maximum(0, requested))
|
||||
if E.gt(net_cost, 0):
|
||||
admitted = E.minimum(admitted, E.floor_value(E.divide(budget.free.get(home, 0), net_cost)))
|
||||
if budget.has("habitats"):
|
||||
var housing: Dictionary = budget.habitats
|
||||
var new_req: Dictionary = housing.requirements.get(destination, {})
|
||||
var old_req: Dictionary = housing.requirements.get(source, {})
|
||||
if not new_req.is_empty():
|
||||
var net_units: float = new_req.units - (old_req.units if old_req.get("id", "") == new_req.id else 0.0)
|
||||
if net_units > 0.0:
|
||||
var free_units: float = maxf(0.0, housing.capacity.get(new_req.id, 0.0) - housing.requested.get(new_req.id, 0.0))
|
||||
admitted = minf(admitted, floorf(free_units / net_units))
|
||||
if not old_req.is_empty(): housing.requested[old_req.id] = maxf(0.0, housing.requested.get(old_req.id, 0.0) - admitted * old_req.units)
|
||||
if not new_req.is_empty(): housing.requested[new_req.id] = housing.requested.get(new_req.id, 0.0) + admitted * new_req.units
|
||||
if old_home != "":
|
||||
budget.requested[old_home] = maxf(0.0, budget.requested.get(old_home, 0.0) - admitted * old_cost)
|
||||
budget.free[old_home] = maxf(0.0, budget.capacity.get(old_home, 0.0) - budget.requested[old_home])
|
||||
budget.requested[home] = budget.requested.get(home, 0.0) + admitted * cost
|
||||
budget.free[home] = maxf(0.0, budget.capacity.get(home, 0.0) - budget.requested[home])
|
||||
if (not new_req.is_empty()):
|
||||
var net_units: Variant = E.subtract(new_req.units, ((old_req.units if E.eq(old_req.get("id", ""), new_req.id) else 0)))
|
||||
if E.gt(net_units, 0):
|
||||
var free_units: Variant = E.maximum(0, E.subtract(housing.capacity.get(new_req.id, 0), housing.requested.get(new_req.id, 0)))
|
||||
admitted = E.minimum(admitted, E.floor_value(E.divide(free_units, net_units)))
|
||||
if (not old_req.is_empty()): housing.requested[old_req.id] = E.maximum(0, E.subtract(housing.requested.get(old_req.id, 0), E.multiply(admitted, old_req.units)))
|
||||
if (not new_req.is_empty()): housing.requested[new_req.id] = E.add(housing.requested.get(new_req.id, 0), E.multiply(admitted, new_req.units))
|
||||
if E.ne(old_home, ""):
|
||||
budget.requested[old_home] = E.maximum(0, E.subtract(budget.requested.get(old_home, 0), E.multiply(admitted, old_cost)))
|
||||
budget.free[old_home] = E.maximum(0, E.subtract(budget.capacity.get(old_home, 0), budget.requested[old_home]))
|
||||
budget.requested[home] = E.add(budget.requested.get(home, 0), E.multiply(admitted, cost))
|
||||
budget.free[home] = E.maximum(0, E.subtract(budget.capacity.get(home, 0), budget.requested[home]))
|
||||
return admitted
|
||||
|
||||
# Resolve shortages from the most advanced stages downwards. This gives existing
|
||||
# advanced populations priority and lets a demotion cascade in the same cycle.
|
||||
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: float) -> void:
|
||||
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void:
|
||||
detach_space_civilizations(target, rules)
|
||||
reconcile(world, target, rules)
|
||||
var budget: Dictionary = stellar_budget(world, target, rules)
|
||||
@@ -114,50 +114,50 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
|
||||
if is_space_civilization(id, rules):
|
||||
stages.append(id)
|
||||
stages.sort()
|
||||
for index in range(stages.size() - 1, -1, -1):
|
||||
for index in range(E.subtract(stages.size(), 1), E.neg(1), E.neg(1)):
|
||||
var id: String = stages[index]
|
||||
var home: String = rules[id].home
|
||||
var cost: float = maxf(1.0, rules[id].stars)
|
||||
var count: float = target.amount.get(id, 0.0)
|
||||
var supported: float = minf(count, floorf(remaining.get(home, 0.0) / cost))
|
||||
var cost: Variant = E.maximum(1, rules[id].stars)
|
||||
var count: Variant = target.amount.get(id, 0)
|
||||
var supported: Variant = E.minimum(count, E.floor_value(E.divide(remaining.get(home, 0), cost)))
|
||||
var requirement: Dictionary = budget.habitats.requirements.get(id, {})
|
||||
if not requirement.is_empty():
|
||||
supported = minf(supported, floorf(remaining_homes.get(requirement.id, 0.0) / requirement.units))
|
||||
remaining_homes[requirement.id] = maxf(0.0, remaining_homes.get(requirement.id, 0.0) - supported * requirement.units)
|
||||
remaining[home] = maxf(0.0, remaining.get(home, 0.0) - supported * cost)
|
||||
var excess: float = count - supported
|
||||
if excess <= 0.0:
|
||||
if (not requirement.is_empty()):
|
||||
supported = E.minimum(supported, E.floor_value(E.divide(remaining_homes.get(requirement.id, 0), requirement.units)))
|
||||
remaining_homes[requirement.id] = E.maximum(0, E.subtract(remaining_homes.get(requirement.id, 0), E.multiply(supported, requirement.units)))
|
||||
remaining[home] = E.maximum(0, E.subtract(remaining.get(home, 0), E.multiply(supported, cost)))
|
||||
var excess: Variant = E.subtract(count, supported)
|
||||
if E.le(excess, 0):
|
||||
continue
|
||||
var previous: String = stages[index - 1] if index > 0 else "C1.9"
|
||||
world.batches.scale_batches(target, id, supported / count)
|
||||
var previous: String = (stages[E.subtract(index, 1)] if E.gt(index, 0) else "C1.9")
|
||||
world.batches.scale_batches(target, id, E.divide(supported, count))
|
||||
target.amount[id] = supported
|
||||
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):
|
||||
if (not target.ethics.has(previous)):
|
||||
target.ethics[previous] = {}
|
||||
for ethic in ETHICS:
|
||||
target.ethics[id][ethic] -= removed[ethic]
|
||||
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
|
||||
if previous == "C1.9":
|
||||
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])
|
||||
if E.eq(previous, "C1.9"):
|
||||
var stars: Dictionary = {}
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
for star in physical:
|
||||
if world.catalog.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
||||
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(physical[star], 0)):
|
||||
stars[star] = physical[star]
|
||||
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
||||
# Worlds are aggregates: a star may host several inhabited planets.
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 0
|
||||
var owners: Array = weights.keys()
|
||||
owners.sort()
|
||||
for i in owners.size():
|
||||
var owner: String = owners[i]
|
||||
var amount: float = floorf(excess * weights[owner]) if i + 1 < owners.size() else excess - assigned
|
||||
assigned += amount
|
||||
if not target.binds.has(owner):
|
||||
var amount: Variant = (E.floor_value(E.multiply(excess, weights[owner])) if E.lt(E.add(i, 1), owners.size()) else E.subtract(excess, assigned))
|
||||
assigned = E.add(assigned, amount)
|
||||
if (not target.binds.has(owner)):
|
||||
target.binds[owner] = {}
|
||||
target.binds[owner][previous] = target.binds[owner].get(previous, 0.0) + amount
|
||||
target.amount[previous] -= amount
|
||||
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
|
||||
target.amount[previous] = E.subtract(target.amount[previous], amount)
|
||||
reconcile(world, target, rules)
|
||||
assign_homes(world, target, rules)
|
||||
|
||||
@@ -168,13 +168,10 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
||||
var structures: Array = []
|
||||
var capacity: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if world.catalog.entities[id].claim != "":
|
||||
if E.ne(world.catalog.entities[id].claim, ""):
|
||||
structures.append(id)
|
||||
capacity[id] = physical.get(id, 0.0)
|
||||
structures.sort_custom(func(a, b):
|
||||
var left: float = world.catalog.entities[a].civ_bonus
|
||||
var right: float = world.catalog.entities[b].civ_bonus
|
||||
return left > right if left != right else str(a) < str(b))
|
||||
capacity[id] = physical.get(id, 0)
|
||||
structures.sort_custom(func ( a , b ) : return (E.gt(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) if E.ne(world.catalog.entities[a].civ_bonus, world.catalog.entities[b].civ_bonus) else E.lt(str(a), str(b))))
|
||||
var stages: Array = []
|
||||
for id in rules:
|
||||
if is_space_civilization(id, rules): stages.append(id)
|
||||
@@ -182,23 +179,23 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
|
||||
var requirements: Dictionary = {}
|
||||
var anchors: Dictionary = {}
|
||||
var anchor: String = ""
|
||||
var units: float = 1.0
|
||||
var units: Variant = 1
|
||||
for id in stages:
|
||||
var direct: String = ""
|
||||
for structure in structures:
|
||||
if world.catalog.entities[structure].claim == id: direct = structure
|
||||
if direct != "":
|
||||
if E.eq(world.catalog.entities[structure].claim, id): direct = structure
|
||||
if E.ne(direct, ""):
|
||||
anchor = direct
|
||||
units = 1.0
|
||||
units = 1
|
||||
else:
|
||||
units *= 10.0
|
||||
units = E.multiply(units, 10)
|
||||
anchors[id] = anchor
|
||||
if id.begins_with("C3.") or id.begins_with("C4."):
|
||||
if (id.begins_with("C3.") or id.begins_with("C4.")):
|
||||
requirements[id] = {"id": anchor, "units": units}
|
||||
var requested: Dictionary = {}
|
||||
for id in requirements:
|
||||
var req: Dictionary = requirements[id]
|
||||
requested[req.id] = requested.get(req.id, 0.0) + physical.get(id, 0.0) * req.units
|
||||
requested[req.id] = E.add(requested.get(req.id, 0), E.multiply(physical.get(id, 0), req.units))
|
||||
return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors}
|
||||
|
||||
static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
@@ -209,29 +206,29 @@ static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
stages.sort()
|
||||
stages.reverse()
|
||||
for id in stages:
|
||||
var left: float = target.amount.get(id, 0.0)
|
||||
var left: Variant = target.amount.get(id, 0)
|
||||
var homes: Dictionary = {}
|
||||
var allowed: bool = budget.anchors[id] == ""
|
||||
var allowed: bool = E.eq(budget.anchors[id], "")
|
||||
for structure in budget.structures:
|
||||
if structure == budget.anchors[id]: allowed = true
|
||||
if not allowed or budget.capacity[structure] <= 0.0: continue
|
||||
var amount: float = left
|
||||
if world.catalog.entities[structure].claim == id:
|
||||
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
|
||||
used[structure] = used.get(structure, 0.0) + amount
|
||||
if amount > 0.0: homes[structure] = amount
|
||||
left -= amount
|
||||
if left <= 0.0: break
|
||||
if E.eq(structure, budget.anchors[id]): allowed = true
|
||||
if ((not allowed) or E.le(budget.capacity[structure], 0)): continue
|
||||
var amount: Variant = left
|
||||
if E.eq(world.catalog.entities[structure].claim, id):
|
||||
amount = E.minimum(amount, E.maximum(0, E.subtract(budget.capacity[structure], used.get(structure, 0))))
|
||||
used[structure] = E.add(used.get(structure, 0), amount)
|
||||
if E.gt(amount, 0): homes[structure] = amount
|
||||
left = E.subtract(left, amount)
|
||||
if E.le(left, 0): break
|
||||
target.civilization_homes[id] = homes
|
||||
|
||||
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
|
||||
if not rules.has(id): return world.catalog.entities[id].lifespan
|
||||
var base: float = rules[id].span
|
||||
if not is_space_civilization(id, rules): return base
|
||||
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> Variant:
|
||||
if (not rules.has(id)): return world.catalog.entities[id].lifespan
|
||||
var base: Variant = rules[id].span
|
||||
if (not is_space_civilization(id, rules)): return base
|
||||
assign_homes(world, target, rules)
|
||||
var population: float = target.amount.get(id, 0.0)
|
||||
if population <= 0.0: return base
|
||||
var weighted: float = population
|
||||
var population: Variant = target.amount.get(id, 0)
|
||||
if E.le(population, 0): return base
|
||||
var weighted: Variant = population
|
||||
for home in target.civilization_homes.get(id, {}):
|
||||
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.catalog.entities[home].civ_bonus) - 1.0)
|
||||
return base * population / weighted if weighted > 0.0 else base
|
||||
weighted = E.add(weighted, E.multiply(target.civilization_homes[id][home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
||||
return (E.divide(E.multiply(base, population), weighted) if E.gt(weighted, 0) else base)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const CivilizationEvolution = preload("res://world/owniverse/civilization_evolution.gd")
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
@@ -12,26 +13,26 @@ func initialize_life_batches() -> void:
|
||||
for id in world.catalog.entities:
|
||||
if world.catalog.entities[id].life_rule.is_empty():
|
||||
continue
|
||||
var tracked: float = 0.0
|
||||
var tracked: Variant = 0
|
||||
for batch : Dictionary in world.state.batches.get(id, []):
|
||||
tracked += batch.amount
|
||||
world.batches.append_batch(world.state, id, maxf(0.0, physical.get(id, 0.0) - tracked), world.state.time.datetime, world.state.time.datetime)
|
||||
tracked = E.add(tracked, batch.amount)
|
||||
world.batches.append_batch(world.state, id, E.maximum(0, E.subtract(physical.get(id, 0), tracked)), world.state.time.datetime, world.state.time.datetime)
|
||||
|
||||
|
||||
func evolute_to_datetime(datetime: float) -> void:
|
||||
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}) -> void:
|
||||
# Reconcile only when simulation advances.
|
||||
# Fractional frame updates cannot change capacity or populations.
|
||||
var processed_year: float = SimulationTime.processed_year(datetime)
|
||||
if processed_year > world.state.time.year:
|
||||
var processed_year: Variant = SimulationTime.processed_year(datetime)
|
||||
if E.gt(processed_year, world.state.time.year):
|
||||
initialize_life_batches()
|
||||
var previous: Dictionary = world.state
|
||||
var next: Dictionary = previous.duplicate_deep()
|
||||
next.version = SimulationTime.STATE_VERSION
|
||||
next.speed = clampi(int(previous.speed), 1, Global.SPEED_LIMIT)
|
||||
next.speed_in_year = pow(10.0, next.speed - 1)
|
||||
var from_year: float = previous.time.year
|
||||
var to_year: float = processed_year
|
||||
if to_year > from_year:
|
||||
next.speed = clampi(E.integer(previous.speed), 1, Global.SPEED_LIMIT)
|
||||
next.speed_in_year = E.power(10, E.subtract(next.speed, 1))
|
||||
var from_year: Variant = previous.time.year
|
||||
var to_year: Variant = processed_year
|
||||
if E.gt(to_year, from_year):
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(previous)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
next.distribution = distribution
|
||||
@@ -40,23 +41,23 @@ func evolute_to_datetime(datetime: float) -> void:
|
||||
evolve_batches(next, to_year)
|
||||
world.ledger.refresh_groups(next)
|
||||
world.ledger.clamp_auto_resources(next)
|
||||
WorldParameters.advance(previous, next, Global.CivilizationRules, to_year - from_year)
|
||||
next.time = {"datetime": datetime, "year": to_year}
|
||||
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
|
||||
next.time = clock if not clock.is_empty() else {"datetime": datetime, "year": to_year}
|
||||
|
||||
# Publish only after every phase has finished; previous stays untouched.
|
||||
world.state = next
|
||||
if to_year > from_year:
|
||||
world.state = E.bound_state(next)
|
||||
if E.gt(to_year, from_year):
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
func evolve_batches(target: Dictionary, to_year: float) -> void:
|
||||
func evolve_batches(target: Dictionary, to_year: Variant) -> void:
|
||||
# A long cycle may contain several generations; preserve actual birth intervals.
|
||||
var completed: Dictionary = {}
|
||||
while evolve_batch_generation(target, to_year, completed):
|
||||
pass
|
||||
|
||||
|
||||
func evolve_batch_generation(target: Dictionary, to_year: float, completed: Dictionary = {}) -> bool:
|
||||
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }) -> bool:
|
||||
# Apply direct completions at the original next-generation boundary, so
|
||||
# binding transfers, the blast environment and capacity checks keep order.
|
||||
var completed_now: Dictionary = completed.duplicate(true)
|
||||
@@ -65,81 +66,81 @@ func evolve_batch_generation(target: Dictionary, to_year: float, completed: Dict
|
||||
var expired: Dictionary = {}
|
||||
var preview: Dictionary = {"batches": {}}
|
||||
for id in target.batches:
|
||||
if world.catalog.entities.has(id) and world.catalog.entities[id].destroy_level > 0:
|
||||
if (world.catalog.entities.has(id) and E.gt(world.catalog.entities[id].destroy_level, 0)):
|
||||
preview.batches[id] = target.batches[id].duplicate(true)
|
||||
for id in target.batches.keys():
|
||||
if not world.catalog.entities.has(id) or world.catalog.entities[id].destroy_level <= 0:
|
||||
if ((not world.catalog.entities.has(id)) or E.le(world.catalog.entities[id].destroy_level, 0)):
|
||||
continue
|
||||
var dying: float = 0.0
|
||||
var dying: Variant = 0
|
||||
for part : Dictionary in completed_now.get(id, []):
|
||||
dying += part.amount
|
||||
dying = E.add(dying, part.amount)
|
||||
for part : Dictionary in world.batches.withdraw_batches(preview, id, to_year):
|
||||
dying += part.amount
|
||||
if quantities.get(id, 0.0) > 0.0:
|
||||
world.stellar.destroy_planets(target, id, dying / quantities[id], world.catalog.entities[id].destroy_level)
|
||||
dying = E.add(dying, part.amount)
|
||||
if E.gt(quantities.get(id, 0), 0):
|
||||
world.stellar.destroy_planets(target, id, E.divide(dying, quantities[id]), world.catalog.entities[id].destroy_level)
|
||||
quantities = world.ledger.physical_amounts(target)
|
||||
# Withdraw all types before creating successors: iteration order cannot affect them.
|
||||
for id in target.batches.keys():
|
||||
if world.catalog.entities.has(id):
|
||||
expired[id] = completed_now.get(id, []) + world.batches.withdraw_batches(target, id, to_year)
|
||||
expired[id] = E.add(completed_now.get(id, []), world.batches.withdraw_batches(target, id, to_year))
|
||||
var has_expired: bool = false
|
||||
for parts : Array in expired.values():
|
||||
if not parts.is_empty():
|
||||
if (not parts.is_empty()):
|
||||
has_expired = true
|
||||
break
|
||||
if not has_expired:
|
||||
if (not has_expired):
|
||||
return false
|
||||
var fractions: Dictionary = {}
|
||||
var outputs: Dictionary = {}
|
||||
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules)
|
||||
var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules)
|
||||
for id in expired:
|
||||
var total: float = 0.0
|
||||
var total: Variant = 0
|
||||
for part : Dictionary in expired[id]:
|
||||
total += part.amount
|
||||
fractions[id] = clampf(total / quantities[id], 0.0, 1.0) if quantities.get(id, 0.0) > 0.0 else 0.0
|
||||
total = E.add(total, part.amount)
|
||||
fractions[id] = (E.clamp_value(E.divide(total, quantities[id]), 0, 1) if E.gt(quantities.get(id, 0), 0) else 0)
|
||||
var combined: Dictionary = {}
|
||||
for part : Dictionary in expired[id]:
|
||||
if not part.has("products"):
|
||||
if (not part.has("products")):
|
||||
part.products = world.stellar.evolution_products(target, world.catalog.entities[id], part.amount)
|
||||
for successor in part.products.keys():
|
||||
if Global.CivilizationRules.has(id):
|
||||
continue
|
||||
var requested: float = part.products[successor]
|
||||
var admitted: float = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
|
||||
var requested: Variant = part.products[successor]
|
||||
var admitted: Variant = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
|
||||
part.products[successor] = admitted
|
||||
if admitted < requested:
|
||||
part.products[id] = part.products.get(id, 0.0) + requested - admitted
|
||||
if E.lt(admitted, requested):
|
||||
part.products[id] = E.subtract(E.add(part.products.get(id, 0), requested), admitted)
|
||||
for successor in part.products:
|
||||
combined[successor] = combined.get(successor, 0.0) + part.products[successor]
|
||||
combined[successor] = E.add(combined.get(successor, 0), part.products[successor])
|
||||
outputs[id] = {}
|
||||
if total > 0.0:
|
||||
if E.gt(total, 0):
|
||||
for successor in combined:
|
||||
outputs[id][successor] = combined[successor] / total
|
||||
outputs[id][successor] = E.divide(combined[successor], total)
|
||||
|
||||
var incoming: Dictionary = world.ledger.transfer_evolution(target, fractions, outputs)
|
||||
|
||||
var new_owners: Dictionary = {}
|
||||
var blast_power: float = 0.0
|
||||
var blast_power: Variant = 0
|
||||
var environment: Dictionary = world.environment.stellar_environment(target)
|
||||
for id in expired:
|
||||
for part : Dictionary in expired[id]:
|
||||
for successor in part.products:
|
||||
var amount: float = part.products[successor]
|
||||
if successor == "blast":
|
||||
blast_power += amount
|
||||
var amount: Variant = part.products[successor]
|
||||
if E.eq(successor, "blast"):
|
||||
blast_power = E.add(blast_power, amount)
|
||||
continue
|
||||
if not world.catalog.entities.has(successor):
|
||||
if (not world.catalog.entities.has(successor)):
|
||||
continue
|
||||
if successor == id and (not world.catalog.entities[id].life_rule.is_empty() or Global.CivilizationRules.has(id)):
|
||||
target.amount[id] = target.amount.get(id, 0.0) + amount
|
||||
if (E.eq(successor, id) and (((not world.catalog.entities[id].life_rule.is_empty()) or Global.CivilizationRules.has(id)))):
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), amount)
|
||||
else:
|
||||
amount = world.ledger.credit(target, successor, amount)
|
||||
var before_batches: Array = target.batches.get(successor, []).duplicate(true)
|
||||
var cutoff: float = to_year if world.stellar.uses_short_lived_fast_path(successor) else -INF
|
||||
var integral: float = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||
var cutoff: Variant = (to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF))
|
||||
var integral: Variant = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
|
||||
new_owners[successor] = new_owners.get(successor, 0.0) + amount
|
||||
new_owners[successor] = E.add(new_owners.get(successor, 0), amount)
|
||||
world.ledger.accept_transfers(target, incoming)
|
||||
world.ledger.bind_new_owners(target, new_owners)
|
||||
world.ledger.round_object_allocations(target)
|
||||
|
||||
@@ -1,101 +1,90 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const GREEN = "#40cc40"
|
||||
const RED = "#ff6060"
|
||||
const GREY = "#888888"
|
||||
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
|
||||
static func number(value: float, digits: int = 0) -> String:
|
||||
if not is_finite(value):
|
||||
return "—"
|
||||
if absf(value) >= 1e5:
|
||||
var exponent: int = int(floor(log(absf(value)) / log(10.0)))
|
||||
return "%.4fE%+d" % [value / pow(10.0, exponent), exponent]
|
||||
return "%.*f" % [digits, value]
|
||||
static func number(value, digits: int = 0) -> String:
|
||||
if not E.finite(value): return "—"
|
||||
return E.scientific(value, 4) if E.ge(E.absolute(value), 100000) else E.fixed(value, digits)
|
||||
|
||||
static func colored(text: String, color: String) -> String:
|
||||
return "[color=" + color + "]" + text + "[/color]"
|
||||
return E.add(E.add(E.add(E.add("[color=", color), "]"), text), "[/color]")
|
||||
|
||||
static func matches(world, id: String) -> Array:
|
||||
var result: Array = []
|
||||
for key in world.catalog.entities:
|
||||
var entity = world.catalog.entities[key]
|
||||
if key == id or entity.count_as == id or entity.group_as == id:
|
||||
if not world.catalog.is_group(key):
|
||||
if ((E.eq(key, id) or E.eq(entity.count_as, id)) or E.eq(entity.group_as, id)):
|
||||
if (not world.catalog.is_group(key)):
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
static func quantities(world, id: String) -> Dictionary:
|
||||
var free: float = 0.0
|
||||
var total: float = 0.0
|
||||
var free: Variant = 0
|
||||
var total: Variant = 0
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for key in matches(world, id):
|
||||
free += world.state.amount.get(key, 0.0)
|
||||
total += physical.get(key, 0.0)
|
||||
if id == "SMBH":
|
||||
free = world.state.amount.get(id, 0.0)
|
||||
free = E.add(free, world.state.amount.get(key, 0))
|
||||
total = E.add(total, physical.get(key, 0))
|
||||
if E.eq(id, "SMBH"):
|
||||
free = world.state.amount.get(id, 0)
|
||||
total = free
|
||||
return {"free": free, "total": total}
|
||||
|
||||
static func costs(world, global: Node, id: String) -> String:
|
||||
if not world.catalog.entities.has(id):
|
||||
if (not world.catalog.entities.has(id)):
|
||||
return ""
|
||||
var lines: PackedStringArray = []
|
||||
for requirements in [world.catalog.entities[id].cost, world.catalog.entities[id].bind]:
|
||||
for resource in requirements:
|
||||
var required: float = requirements[resource]
|
||||
if required < 0.0:
|
||||
var required: Variant = requirements[resource]
|
||||
if E.lt(required, 0):
|
||||
continue
|
||||
var available: float = floorf(world.ledger.available(world.state, resource))
|
||||
var available: Variant = E.floor_value(world.ledger.available(world.state, resource))
|
||||
var name: String = str(global.get_template(resource, "Entity", "Name").get("text", resource))
|
||||
lines.append(colored(name + ": " + number(required) + " (" + number(available) + ")", GREEN if available >= required else RED))
|
||||
lines.append(colored(E.add(E.add(E.add(E.add(E.add(name, ": "), number(required)), " ("), number(available)), ")"), (GREEN if E.ge(available, required) else RED)))
|
||||
return "\n".join(lines)
|
||||
|
||||
static func variables(world, global: Node, id: String, rate: float = 0.0) -> Dictionary:
|
||||
static func variables(world, global: Node, id: String, rate: Variant = 0.0) -> Dictionary:
|
||||
var counts: Dictionary = quantities(world, id)
|
||||
var show_total: bool = false
|
||||
for key in matches(world, id):
|
||||
show_total = show_total or world.catalog.entities[key].group_as == "Planet"
|
||||
var amount: String = number(counts.total if show_total else counts.free) if world.catalog.entities.has(id) else "—"
|
||||
if not show_total and counts.total > counts.free:
|
||||
amount += " " + colored("(" + number(counts.total) + ")", GREY)
|
||||
if rate != 0.0:
|
||||
amount += " | " + colored(("−" if rate < 0.0 else "+") + number(absf(rate), 1), RED if rate < 0.0 else GREEN)
|
||||
show_total = (show_total or E.eq(world.catalog.entities[key].group_as, "Planet"))
|
||||
var amount: String = (number((counts.total if show_total else counts.free)) if world.catalog.entities.has(id) else "—")
|
||||
if ((not show_total) and E.gt(counts.total, counts.free)):
|
||||
amount = E.add(amount, E.add(" ", colored(E.add(E.add("(", number(counts.total)), ")"), GREY)))
|
||||
if E.ne(rate, 0):
|
||||
amount = E.add(amount, E.add(" | ", colored(E.add((("−" if E.lt(rate, 0) else "+")), number(E.absolute(rate), 1)), (RED if E.lt(rate, 0) else GREEN))))
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
var voids: float = 0.0
|
||||
var voids: Variant = 0
|
||||
for key in physical:
|
||||
voids += physical[key] * world.catalog.entities[key].voids
|
||||
var he: float = world.state.amount.get("He", 0.0)
|
||||
var h: float = world.state.amount.get("H", 0.0)
|
||||
voids = E.add(voids, E.multiply(physical[key], world.catalog.entities[key].voids))
|
||||
var he: Variant = world.state.amount.get("He", 0)
|
||||
var h: Variant = world.state.amount.get("H", 0)
|
||||
var mixture: String = "—"
|
||||
if he + h > 0.0:
|
||||
mixture = "He: " + number(100.0 * he / (he + h), 2) + "% / H: " + number(100.0 * h / (he + h), 2) + "%"
|
||||
if E.gt(E.add(he, h), 0):
|
||||
mixture = E.add(E.add(E.add(E.add("He: ", number(E.divide(E.multiply(100, he), (E.add(he, h))), 2)), "% / H: "), number(E.divide(E.multiply(100, h), (E.add(he, h))), 2)), "%")
|
||||
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||
return {
|
||||
"amount": amount, "colored costs": costs(world, global, id),
|
||||
"lifespan": number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—",
|
||||
"voids": number(voids), "he / h": "\n" + mixture,
|
||||
"metallicity": number(environment.metallicity * 100.0, 2) + "%",
|
||||
"энергия": number(world.state.energy), "плотность": number(environment.density, 6),
|
||||
"лет в секунду": number(world.state.speed_in_year),
|
||||
"amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—",
|
||||
}
|
||||
return {"amount": amount, "colored costs": costs(world, global, id), "lifespan": (number(world.catalog.entities[id].lifespan) if world.catalog.entities.has(id) else "—"), "voids": number(voids), "he / h": E.add("\n", mixture), "metallicity": E.add(number(E.multiply(environment.metallicity, 100), 2), "%"), "энергия": number(world.state.energy), "плотность": number(environment.density, 6), "лет в секунду": number(world.state.speed_in_year), "amount1": "—", "amount2": "—", "amount3": "—", "amount4": "—"}
|
||||
|
||||
static func render(world, global: Node, id: String, category: String = "Entity", rate: float = 0.0) -> String:
|
||||
static func render(world, global: Node, id: String, category: String = "Entity", rate: Variant = 0.0) -> String:
|
||||
var values: Dictionary = variables(world, global, id, rate)
|
||||
if category == "Indicator":
|
||||
if id == "Density":
|
||||
if E.eq(category, "Indicator"):
|
||||
if E.eq(id, "Density"):
|
||||
id = Parameters.density_display(world.environment.stellar_environment(world.state).density).mode
|
||||
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
|
||||
if scales.has(id):
|
||||
var level: float = Parameters.value(world.state, scales[id])
|
||||
values.amount = number(level, 1 if level != floorf(level) else 0)
|
||||
values.scale = global.render_template(global.get_template("Scale" + str(clampi(roundi(level), 1, 19)), "Indicator", "Scale"), {})
|
||||
if id == "Trends":
|
||||
var level: Variant = Parameters.value(world.state, scales[id])
|
||||
values.amount = number(level, (1 if E.ne(level, E.floor_value(level)) else 0))
|
||||
values.scale = global.render_template(global.get_template(E.add("Scale", str(clampi(E.integer(E.round_value(level)), 1, 19))), "Indicator", "Scale"), {})
|
||||
if E.eq(id, "Trends"):
|
||||
for trend in Parameters.TRENDS:
|
||||
values[trend.to_lower()] = number(world.state.get("trends", {}).get(trend, 0.0) * 100.0, 2) + "%"
|
||||
if id == "Energy":
|
||||
values["энергия"] = number(world.state.energy) + " (" + number(clampf(world.state.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0) * 100.0, 2) + "%)"
|
||||
values[trend.to_lower()] = E.add(number(E.multiply(world.state.get("trends", {}).get(trend, 0), 100), 2), "%")
|
||||
if E.eq(id, "Energy"):
|
||||
values["энергия"] = E.add(E.add(E.add(number(world.state.energy), " ("), number(E.multiply(E.clamp_value(E.divide(world.state.energy, Parameters.INITIAL_ENERGY), 0, 1), 100), 2)), "%)")
|
||||
var title: String = global.render_template(global.get_template(id, category, "Name"), values)
|
||||
var body: String = global.render_template(global.get_template(id, category, "Description"), values)
|
||||
return "[b]" + (title if title != "" else id) + "[/b]\n" + body
|
||||
return E.add(E.add(E.add("[b]", ((title if E.ne(title, "") else id))), "[/b]\n"), body)
|
||||
|
||||
@@ -1,54 +1,55 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Read-only projections of simulation state. No controls or simulation mutations.
|
||||
|
||||
var pending_amounts: Dictionary[String, float] = {}
|
||||
var pending_amounts: Dictionary [ String , Variant ] = {}
|
||||
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:
|
||||
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:
|
||||
static func amounts(state: Dictionary, catalog, total_produced: bool = false) -> Dictionary [ String, Variant ]:
|
||||
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))
|
||||
if (not catalog.is_group(id)):
|
||||
physical[id] = E.maximum(0, state.amount.get(id, 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
|
||||
physical[id] = E.add(physical[id], E.maximum(0, bindings[id]))
|
||||
var planets: Variant = 0
|
||||
for id in physical:
|
||||
if catalog.entities[id].group_as == "Planet":
|
||||
if E.eq(catalog.entities[id].group_as, "Planet"):
|
||||
source[id] = physical[id]
|
||||
planets += physical[id]
|
||||
planets = E.add(planets, physical[id])
|
||||
source["Planet"] = planets
|
||||
var result: Dictionary[String, float] = {}
|
||||
var result: Dictionary [ String , Variant ] = {}
|
||||
for id in source:
|
||||
result[id] = 0.0
|
||||
result[id] = 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 != "":
|
||||
if (catalog.entities.has(id) and E.ne(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])
|
||||
if E.ne(item_id, id):
|
||||
result[id] = E.add(result.get(id, 0), E.of(source[id]))
|
||||
result[item_id] = E.add(result.get(item_id, 0), E.of(source[id]))
|
||||
for id in source:
|
||||
if catalog.is_group(id):
|
||||
result[id] = float(source[id])
|
||||
result[id] = E.of(source[id])
|
||||
return result
|
||||
|
||||
|
||||
@@ -63,12 +64,12 @@ static func unlocks(state: Dictionary, catalog, locked: Array) -> Dictionary:
|
||||
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]:
|
||||
if E.lt(E.maximum(state.stats.total_amount.get(requirement, 0), display_totals.get(requirement, 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):
|
||||
if (E.ne(item_id, "") and (not visible.has(item_id))):
|
||||
visible.append(item_id)
|
||||
else:
|
||||
remaining.append(id)
|
||||
|
||||
@@ -1,58 +1,56 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func life_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||
func life_products(target: Dictionary, entity: OwniverseEntity, count: Variant) -> Dictionary:
|
||||
var rule: Dictionary = entity.life_rule
|
||||
var pending: Dictionary = {}
|
||||
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
|
||||
var probabilities: Dictionary = {
|
||||
"progress": float(rule.progress) * modifiers.progress * WorldParameters.value(target, "Enlightenment"),
|
||||
"degradation": float(rule.degradation) * modifiers.degradation,
|
||||
}
|
||||
var probability_sum: float = maxf(1.0, probabilities.progress + probabilities.degradation)
|
||||
if not target.has("life_remainders"):
|
||||
var probabilities: Dictionary = {"progress": E.multiply(E.multiply(E.of(rule.progress), modifiers.progress), WorldParameters.value(target, "Enlightenment")), "degradation": E.multiply(E.of(rule.degradation), modifiers.degradation)}
|
||||
var probability_sum: Variant = E.maximum(1, E.add(probabilities.progress, probabilities.degradation))
|
||||
if (not target.has("life_remainders")):
|
||||
target.life_remainders = {}
|
||||
for channel in ["progress", "degradation"]:
|
||||
var key: String = entity.id + ":" + channel
|
||||
var expected: float = count * probabilities[channel] / probability_sum + target.life_remainders.get(key, 0.0)
|
||||
var key: String = E.add(E.add(entity.id, ":"), channel)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(count, probabilities[channel]), probability_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
pending[channel] = floorf(snappedf(expected, 1.0e-12))
|
||||
pending[channel] = E.floor_value(expected)
|
||||
var assigned: Dictionary = QuantityMath.allocate_whole(pending, count)
|
||||
for channel in assigned:
|
||||
var key: String = entity.id + ":" + channel
|
||||
target.life_remainders[key] = maxf(0.0, target.life_remainders[key] - assigned[channel])
|
||||
var result: Dictionary = {entity.id: count - assigned.progress - assigned.degradation}
|
||||
var key: String = E.add(E.add(entity.id, ":"), channel)
|
||||
target.life_remainders[key] = E.maximum(0, E.subtract(target.life_remainders[key], assigned[channel]))
|
||||
var result: Dictionary = {entity.id: E.subtract(E.subtract(count, assigned.progress), assigned.degradation)}
|
||||
result[rule.next] = assigned.progress
|
||||
var weights: Dictionary = {}
|
||||
var weight_sum: float = 0.0
|
||||
var weight_sum: Variant = 0
|
||||
for term in str(rule.fall).split("^"):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
weights[pair[0]] = float(pair[1])
|
||||
weight_sum += float(pair[1])
|
||||
weights[pair[0]] = E.of(pair[1])
|
||||
weight_sum = E.add(weight_sum, E.of(pair[1]))
|
||||
var requests: Dictionary = {}
|
||||
for id in weights:
|
||||
var key: String = entity.id + ":fall:" + id
|
||||
var expected: float = assigned.degradation * weights[id] / weight_sum + target.life_remainders.get(key, 0.0)
|
||||
var key: String = E.add(E.add(entity.id, ":fall:"), id)
|
||||
var expected: Variant = E.add(E.divide(E.multiply(assigned.degradation, weights[id]), weight_sum), target.life_remainders.get(key, 0))
|
||||
target.life_remainders[key] = expected
|
||||
requests[id] = maxf(0.0, floorf(snappedf(expected, 1.0e-12)))
|
||||
requests[id] = E.maximum(0, E.floor_value(expected))
|
||||
var falls: Dictionary = QuantityMath.allocate_whole(requests, assigned.degradation)
|
||||
var distributed: float = 0.0
|
||||
var distributed: Variant = 0
|
||||
for id in falls:
|
||||
distributed += falls[id]
|
||||
result[id] = result.get(id, 0.0) + falls[id]
|
||||
var key: String = entity.id + ":fall:" + id
|
||||
target.life_remainders[key] -= falls[id]
|
||||
distributed = E.add(distributed, falls[id])
|
||||
result[id] = E.add(result.get(id, 0), falls[id])
|
||||
var key: String = E.add(E.add(entity.id, ":fall:"), id)
|
||||
target.life_remainders[key] = E.subtract(target.life_remainders[key], falls[id])
|
||||
# Fill sub-unit fall quotas deterministically, retaining signed allocation errors.
|
||||
while distributed < assigned.degradation:
|
||||
while E.lt(distributed, assigned.degradation):
|
||||
var selected: String = ""
|
||||
var largest: float = -INF
|
||||
var largest: Variant = E.neg(INF)
|
||||
for id in weights:
|
||||
var rest: float = target.life_remainders[entity.id + ":fall:" + id]
|
||||
if rest > largest:
|
||||
var rest: Variant = target.life_remainders[E.add(E.add(entity.id, ":fall:"), id)]
|
||||
if E.gt(rest, largest):
|
||||
largest = rest
|
||||
selected = id
|
||||
result[selected] = result.get(selected, 0.0) + 1.0
|
||||
target.life_remainders[entity.id + ":fall:" + selected] -= 1.0
|
||||
distributed += 1.0
|
||||
result[selected] = E.add(result.get(selected, 0), 1)
|
||||
target.life_remainders[E.add(E.add(entity.id, ":fall:"), selected)] = E.subtract(target.life_remainders[E.add(E.add(entity.id, ":fall:"), selected)], 1)
|
||||
distributed = E.add(distributed, 1)
|
||||
return result
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
|
||||
automatic: bool = false) -> bool:
|
||||
if amount <= 0.0 or amount != floorf(amount):
|
||||
func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant, automatic: bool = false) -> bool:
|
||||
if (E.le(amount, 0) or E.ne(amount, E.floor_value(amount))):
|
||||
return false
|
||||
var trial: Dictionary = target.duplicate()
|
||||
trial.amount = target.amount.duplicate()
|
||||
@@ -13,26 +13,26 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: float,
|
||||
trial.batches = target.batches.duplicate()
|
||||
var copied_batches: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
var cost: float = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||
if automatic and trial.auto_resources.get(resource, 0.0) < cost:
|
||||
var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
|
||||
if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)):
|
||||
return false
|
||||
var before: Dictionary = world.ledger.physical_amounts(trial)
|
||||
var taken: Dictionary = world.ledger.take_available(trial, resource, cost)
|
||||
var total: float = 0.0
|
||||
var total: Variant = 0
|
||||
for quantity in taken.values():
|
||||
total += quantity
|
||||
if total != cost:
|
||||
total = E.add(total, quantity)
|
||||
if E.ne(total, cost):
|
||||
return false
|
||||
if automatic:
|
||||
trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost)
|
||||
trial.auto_resources[resource] = E.maximum(0, E.subtract(trial.auto_resources.get(resource, 0), cost))
|
||||
for id in taken:
|
||||
# Isolate only batches actually touched by this transaction.
|
||||
if trial.batches.has(id) and not copied_batches.has(id):
|
||||
if (trial.batches.has(id) and (not copied_batches.has(id))):
|
||||
trial.batches[id] = trial.batches[id].duplicate(true)
|
||||
copied_batches[id] = true
|
||||
var factor: float = 1.0 - taken[id] / before[id] if before.get(id, 0.0) > 0.0 else 0.0
|
||||
var factor: Variant = (E.subtract(1, E.divide(taken[id], before[id])) if E.gt(before.get(id, 0), 0) else 0)
|
||||
world.batches.scale_batches(trial, id, factor)
|
||||
if not world.ledger.bind_new_owners(trial, {entity.id: amount}, true):
|
||||
if (not world.ledger.bind_new_owners(trial, {entity.id: amount}, true)):
|
||||
return false
|
||||
world.ledger.refresh_groups(trial)
|
||||
world.ledger.clamp_auto_resources(trial)
|
||||
@@ -46,72 +46,71 @@ func smbh_sources() -> Dictionary:
|
||||
var masses: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if entity.count_as == "BH" and entity.mass > 0.0:
|
||||
masses[id] = floorf(maxf(0.0, world.state.amount.get(id, 0.0))) * entity.mass
|
||||
if (E.eq(entity.count_as, "BH") and E.gt(entity.mass, 0)):
|
||||
masses[id] = E.multiply(E.floor_value(E.maximum(0, world.state.amount.get(id, 0))), entity.mass)
|
||||
return masses
|
||||
|
||||
|
||||
func smbh_available() -> float:
|
||||
var mass: float = 0.0
|
||||
func smbh_available() -> Variant:
|
||||
var mass: Variant = 0
|
||||
for value in smbh_sources().values():
|
||||
mass += value
|
||||
return floorf(mass)
|
||||
mass = E.add(mass, value)
|
||||
return E.floor_value(mass)
|
||||
|
||||
|
||||
func plan_smbh(desired_amount: float) -> Dictionary:
|
||||
var empty: Dictionary = {"amount": 0.0, "taken": {}}
|
||||
if desired_amount <= 0.0 or not is_finite(desired_amount):
|
||||
func plan_smbh(desired_amount: Variant) -> Dictionary:
|
||||
var empty: Dictionary = {"amount": 0, "taken": {}}
|
||||
if (E.le(desired_amount, 0) or (not E.finite(desired_amount))):
|
||||
return empty
|
||||
var total_mass: float = smbh_available()
|
||||
if total_mass <= 0.0:
|
||||
var total_mass: Variant = smbh_available()
|
||||
if E.le(total_mass, 0):
|
||||
return empty
|
||||
var requested: float = minf(floorf(desired_amount), total_mass)
|
||||
if requested <= 0.0:
|
||||
var requested: Variant = E.minimum(E.floor_value(desired_amount), total_mass)
|
||||
if E.le(requested, 0):
|
||||
return empty
|
||||
var masses: Dictionary = smbh_sources()
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(masses)
|
||||
var taken: Dictionary = {}
|
||||
var order: Array = []
|
||||
var consumed_mass: float = 0.0
|
||||
var consumed_mass: Variant = 0
|
||||
for id in distribution:
|
||||
var expected: float = requested * distribution[id] / world.catalog.entities[id].mass
|
||||
taken[id] = minf(floorf(expected), floorf(world.state.amount[id]))
|
||||
consumed_mass += taken[id] * world.catalog.entities[id].mass
|
||||
order.append({"id": id, "remainder": expected - taken[id]})
|
||||
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.remainder > b.remainder if a.remainder != b.remainder else str(a.id) < str(b.id))
|
||||
var expected: Variant = E.divide(E.multiply(requested, distribution[id]), world.catalog.entities[id].mass)
|
||||
taken[id] = E.minimum(E.floor_value(expected), E.floor_value(world.state.amount[id]))
|
||||
consumed_mass = E.add(consumed_mass, E.multiply(taken[id], world.catalog.entities[id].mass))
|
||||
order.append({"id": id, "remainder": E.subtract(expected, taken[id])})
|
||||
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.remainder, b.remainder) if E.ne(a.remainder, b.remainder) else E.lt(str(a.id), str(b.id))))
|
||||
# Consume whole BH only; all excess mass becomes SMBH, never disappears.
|
||||
for candidate : Dictionary in order:
|
||||
if consumed_mass >= requested:
|
||||
if E.ge(consumed_mass, requested):
|
||||
break
|
||||
var id: String = candidate.id
|
||||
if taken[id] < floorf(world.state.amount[id]):
|
||||
taken[id] += 1.0
|
||||
consumed_mass += world.catalog.entities[id].mass
|
||||
if consumed_mass <= 0.0 or consumed_mass != floorf(consumed_mass):
|
||||
if E.lt(taken[id], E.floor_value(world.state.amount[id])):
|
||||
taken[id] = E.add(taken[id], 1)
|
||||
consumed_mass = E.add(consumed_mass, world.catalog.entities[id].mass)
|
||||
if (E.le(consumed_mass, 0) or E.ne(consumed_mass, E.floor_value(consumed_mass))):
|
||||
return empty
|
||||
return {"amount": consumed_mass, "taken": taken}
|
||||
|
||||
|
||||
func produce_smbh(desired_amount: float) -> float:
|
||||
func produce_smbh(desired_amount: Variant) -> Variant:
|
||||
var plan: Dictionary = plan_smbh(desired_amount)
|
||||
var consumed_mass: float = plan.amount
|
||||
var consumed_mass: Variant = plan.amount
|
||||
var taken: Dictionary = plan.taken
|
||||
if consumed_mass <= 0.0:
|
||||
return 0.0
|
||||
if E.le(consumed_mass, 0):
|
||||
return 0
|
||||
var next: Dictionary = world.state.duplicate(true)
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for id in taken:
|
||||
if taken[id] <= 0.0:
|
||||
if E.le(taken[id], 0):
|
||||
continue
|
||||
next.amount[id] -= taken[id]
|
||||
var fraction: float = taken[id] / physical[id]
|
||||
world.batches.scale_batches(next, id, 1.0 - fraction)
|
||||
next.amount[id] = E.subtract(next.amount[id], taken[id])
|
||||
var fraction: Variant = E.divide(taken[id], physical[id])
|
||||
world.batches.scale_batches(next, id, E.subtract(1, fraction))
|
||||
# Release the consumed owners' share of any resources they held.
|
||||
for resource in next.binds.get(id, {}):
|
||||
var released: float = next.binds[id][resource] * fraction
|
||||
next.binds[id][resource] -= released
|
||||
next.amount[resource] = next.amount.get(resource, 0.0) + released
|
||||
var released: Variant = E.multiply(next.binds[id][resource], fraction)
|
||||
next.binds[id][resource] = E.subtract(next.binds[id][resource], released)
|
||||
next.amount[resource] = E.add(next.amount.get(resource, 0), released)
|
||||
world.ledger.credit(next, "SMBH", consumed_mass)
|
||||
world.batches.append_batch(next, "SMBH", consumed_mass, world.state.time.datetime, world.state.time.datetime)
|
||||
world.ledger.round_object_allocations(next)
|
||||
@@ -122,28 +121,28 @@ func produce_smbh(desired_amount: float) -> float:
|
||||
return consumed_mass
|
||||
|
||||
|
||||
func produce(item_id: String, desired_amount: float) -> float:
|
||||
if item_id == "SMBH":
|
||||
func produce(item_id: String, desired_amount: Variant) -> Variant:
|
||||
if E.eq(item_id, "SMBH"):
|
||||
return produce_smbh(desired_amount)
|
||||
# Use special production for S and H
|
||||
if item_id == "S" or item_id == "H":
|
||||
if (E.eq(item_id, "S") or E.eq(item_id, "H")):
|
||||
return produce_hs(item_id, desired_amount)
|
||||
|
||||
var entity: OwniverseEntity = world.catalog.entities[item_id]
|
||||
|
||||
if desired_amount <= 0:
|
||||
return 0.0
|
||||
if E.le(desired_amount, 0):
|
||||
return 0
|
||||
|
||||
var max_producible = floorf(calculate_max_producible(entity, desired_amount))
|
||||
var max_producible = E.floor_value(calculate_max_producible(entity, desired_amount))
|
||||
var budget: Dictionary = CivilizationState.stellar_budget(world, world.state, Global.CivilizationRules)
|
||||
max_producible = CivilizationState.admit_transition(budget, Global.CivilizationRules, "", item_id, max_producible)
|
||||
|
||||
if max_producible <= 0:
|
||||
return 0.0
|
||||
if E.le(max_producible, 0):
|
||||
return 0
|
||||
|
||||
var metallicity: float = world.environment.stellar_environment(world.state).metallicity
|
||||
if not try_production(world.state, entity, max_producible):
|
||||
return 0.0
|
||||
var metallicity: Variant = world.environment.stellar_environment(world.state).metallicity
|
||||
if (not try_production(world.state, entity, max_producible)):
|
||||
return 0
|
||||
|
||||
# Add produced entities
|
||||
world.ledger.process_entity(item_id, max_producible)
|
||||
@@ -154,49 +153,49 @@ func produce(item_id: String, desired_amount: float) -> float:
|
||||
return max_producible
|
||||
|
||||
|
||||
func produce_hs(entity_id: String, value: float) -> float:
|
||||
if value <= 0:
|
||||
return 0.0
|
||||
func produce_hs(entity_id: String, value: Variant) -> Variant:
|
||||
if E.le(value, 0):
|
||||
return 0
|
||||
|
||||
var to_produce: float = floorf(value)
|
||||
to_produce = minf(to_produce, floorf(world.state.energy))
|
||||
var to_produce: Variant = E.floor_value(value)
|
||||
to_produce = E.minimum(to_produce, E.floor_value(world.state.energy))
|
||||
|
||||
if to_produce <= 0:
|
||||
return 0.0
|
||||
if E.le(to_produce, 0):
|
||||
return 0
|
||||
|
||||
world.state.energy -= to_produce
|
||||
world.state.energy = E.subtract(world.state.energy, to_produce)
|
||||
world.ledger.process_entity(entity_id, to_produce)
|
||||
|
||||
return to_produce
|
||||
|
||||
|
||||
func production_step(entity: OwniverseEntity) -> float:
|
||||
return QuantityMath.production_step(entity.cost.values() + entity.bind.values())
|
||||
func production_step(entity: OwniverseEntity) -> Variant:
|
||||
return QuantityMath.production_step(E.add(entity.cost.values(), entity.bind.values()))
|
||||
|
||||
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
|
||||
if entity.id == "SMBH":
|
||||
func calculate_max_producible(entity: OwniverseEntity, desired_amount: Variant = 1.0E150) -> Variant:
|
||||
if E.eq(entity.id, "SMBH"):
|
||||
return plan_smbh(desired_amount).amount
|
||||
var requirements: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
requirements[resource] = maxf(0.0, entity.cost[resource])
|
||||
requirements[resource] = E.maximum(0, entity.cost[resource])
|
||||
for resource in entity.bind:
|
||||
requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource])
|
||||
requirements[resource] = E.add(requirements.get(resource, 0), E.maximum(0, entity.bind[resource]))
|
||||
var has_requirements: bool = false
|
||||
for quantity in requirements.values():
|
||||
if quantity > 0.0:
|
||||
if E.gt(quantity, 0):
|
||||
has_requirements = true
|
||||
break
|
||||
# No recipe means unavailable for production, not unlimited free objects.
|
||||
if not has_requirements:
|
||||
return 0.0
|
||||
var available: float = maxf(0.0, desired_amount)
|
||||
if (not has_requirements):
|
||||
return 0
|
||||
var available: Variant = E.maximum(0, desired_amount)
|
||||
for resource in requirements:
|
||||
if requirements[resource] > 0.0:
|
||||
available = minf(available, floorf(world.ledger.available(world.state, resource) / requirements[resource]))
|
||||
if E.gt(requirements[resource], 0):
|
||||
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), requirements[resource])))
|
||||
var concrete: Dictionary = world.ledger.expand_requirements(world.state, requirements)
|
||||
for resource in concrete:
|
||||
if concrete[resource] > 0.0:
|
||||
available = minf(available, floorf(world.ledger.available(world.state, resource) / concrete[resource]))
|
||||
var step: float = production_step(entity)
|
||||
return floorf(available / step) * step if is_finite(step) else 0.0
|
||||
if E.gt(concrete[resource], 0):
|
||||
available = E.minimum(available, E.floor_value(E.divide(world.ledger.available(world.state, resource), concrete[resource])))
|
||||
var step: Variant = production_step(entity)
|
||||
return (E.multiply(E.floor_value(E.divide(available, step)), step) if E.finite(step) else 0)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Числа симуляции
|
||||
|
||||
Количество объектов, запасы, энергия, расходы, привязки и статистика считаются
|
||||
целочисленно, без перехода через `float`. Значения до `10^27` сохраняют каждую
|
||||
единицу; промежуточные произведения могут превышать этот диапазон.
|
||||
|
||||
`big_integer.gd` хранит модуль числа массивом разрядов по основанию `10^9`,
|
||||
младший разряд первым. Переносы при умножении не переполняют `int64`.
|
||||
Деление использует нормализованную оценку цифры частного.
|
||||
|
||||
`exact.gd` — единый API арифметики. Малые значения остаются обычными `int`,
|
||||
большие числа и дроби представлены `exact_value.gd`: знак, целый числитель,
|
||||
целый знаменатель. Дроби сокращаются. Операции создают новые значения;
|
||||
поля `ExactValue` и массивы его разрядов нельзя изменять после создания.
|
||||
|
||||
```gdscript
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
var stock = E.of("1e27") # Строка, не литерал float 1e27!
|
||||
stock = E.subtract(stock, 1) # 999999999999999999999999999
|
||||
var price = E.of("0.125") # Точно 1/8
|
||||
var cost = E.multiply(price, 8) # Точно 1
|
||||
if E.ge(stock, cost):
|
||||
stock = E.subtract(stock, cost)
|
||||
```
|
||||
|
||||
Использовать `E.add/subtract/multiply/divide`, `E.eq/lt/le/gt/ge`,
|
||||
`E.floor_value/ceil_value/round_value`. Перегрузки операторов в GDScript нет:
|
||||
обычные `+`, `==`, `min`, `float(...)` для чисел симуляции не подходят.
|
||||
Сигнатуры принимают `Variant`, а не `float`. `E.integer` допускается только
|
||||
для значений в диапазоне `int64`, например индексов и состояния генератора.
|
||||
Для поиска чисел в массиве используется `E.contains`, поскольку сравнение
|
||||
объектов по ссылке не является сравнением числовых значений.
|
||||
|
||||
## Границы точности
|
||||
|
||||
- Десятичные параметры CSV разбираются из строк точно. Стоимости, проверка
|
||||
доступности и размер целой производственной партии не округляются.
|
||||
- Суммы распределений и переносы целых объектов сохраняются точно.
|
||||
- Промежуточная рациональная арифметика точна. Перед публикацией следующего
|
||||
состояния знаменатели сохранённых дробей ограничиваются `10^36`.
|
||||
Если знаменатель больше, значение усекается к нулю с ошибкой меньше
|
||||
`10^-36`. Целые числа эта операция не меняет. Это предотвращает рост
|
||||
знаменателей при многократном накоплении меняющихся долей. Такая точность
|
||||
относится к каждой операции ограничения, а не является обещанием нулевой
|
||||
суммарной ошибки при бесконечном числе циклов.
|
||||
- Логарифмы, корни, нецелые степени и нормальная случайная выборка остаются
|
||||
приближёнными. Результаты трансцендентных функций переводятся обратно в
|
||||
рациональные коэффициенты с 12 знаками после точки. Статистическая выборка
|
||||
не обеспечивает равномерный выбор каждого отдельного целого около `10^27`;
|
||||
последующий учёт выбранного количества и остатка точен.
|
||||
- Возраст хранится в целых тиках (`10^9` тиков на год). Малый остаток кадрового
|
||||
времени остаётся `float` и переносится между кадрами; большой возраст
|
||||
никогда не складывается с малым приращением через `float`.
|
||||
- Интерфейс преобразует в `float` только координаты индикаторов. Текст чисел,
|
||||
доступное количество и значения кнопок формируются из точных чисел.
|
||||
|
||||
`E.of(float)` сохраняет фактическое двоичное значение входного числа: он
|
||||
не может восстановить единицы, уже потерянные предыдущими вычислениями.
|
||||
|
||||
## Сохранение
|
||||
|
||||
Формат сохранения — **3**, версия состояния — **0.2.0**. На главном потоке
|
||||
`E.encode` строит независимый снимок, затем существующий фоновый писатель
|
||||
сохраняет его. Большие целые и дроби кодируются как `{"$exact": "число"}`;
|
||||
для дробей используется строка `числитель/знаменатель`. Целые за пределами
|
||||
точного диапазона JSON/float тоже кодируются строкой. Зашифрованный файл и
|
||||
диагностический JSON используют один снимок. После проверки `E.decode`
|
||||
восстанавливает числа, без загрузки сериализованных объектов GDScript.
|
||||
|
||||
Миграции старой арифметики нет. Старый формат несовместим: существующий
|
||||
механизм загрузки сохраняет его в `.incompatible.bak` и начинает новую игру.
|
||||
Старое сохранение игрока не используется тестами и не изменялось при разработке.
|
||||
|
||||
## Проверки
|
||||
|
||||
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||
|
||||
Независимый эталон `tests/fixtures/exact_oracle.json` получен через Python
|
||||
`fractions.Fraction` (120 пар, seed 4321). Тест сравнивает строковые результаты
|
||||
суммы, разности, произведения, деления и округления вниз. Дополнительно
|
||||
проверяются границы `int64`, JSON, рецепты, распределения, жизнь при `10^27`,
|
||||
единичный расход энергии, тик времени и 100 циклов с меняющимися долями звёзд.
|
||||
@@ -0,0 +1,102 @@
|
||||
extends RefCounted
|
||||
## Unsigned little-endian base 10^9 arithmetic. Inputs are never mutated.
|
||||
const BASE: int = 1000000000
|
||||
|
||||
static func trim(a: Array) -> Array:
|
||||
while a.size() > 1 and a.back() == 0: a.pop_back()
|
||||
return a
|
||||
|
||||
static func parse(text: String) -> Array:
|
||||
var result: Array = []
|
||||
var end: int = text.length()
|
||||
while end > 0:
|
||||
var start: int = maxi(0, end - 9)
|
||||
result.append(int(text.substr(start, end - start)))
|
||||
end = start
|
||||
return trim(result if not result.is_empty() else [0])
|
||||
|
||||
static func decimal(a: Array) -> String:
|
||||
var result: String = str(a.back())
|
||||
for i in range(a.size() - 2, -1, -1): result += "%09d" % a[i]
|
||||
return result
|
||||
|
||||
static func compare(a: Array, b: Array) -> int:
|
||||
if a.size() != b.size(): return 1 if a.size() > b.size() else -1
|
||||
for i in range(a.size() - 1, -1, -1):
|
||||
if a[i] != b[i]: return 1 if a[i] > b[i] else -1
|
||||
return 0
|
||||
|
||||
static func add(a: Array, b: Array) -> Array:
|
||||
var result: Array = []
|
||||
var carry: int = 0
|
||||
for i in maxi(a.size(), b.size()):
|
||||
var value: int = carry + (a[i] if i < a.size() else 0) + (b[i] if i < b.size() else 0)
|
||||
result.append(value % BASE)
|
||||
carry = value / BASE
|
||||
if carry: result.append(carry)
|
||||
return result
|
||||
|
||||
static func subtract(a: Array, b: Array) -> Array:
|
||||
assert(compare(a, b) >= 0, "Unsigned subtraction underflow")
|
||||
var result: Array = []
|
||||
var borrow: int = 0
|
||||
for i in a.size():
|
||||
var value: int = a[i] - borrow - (b[i] if i < b.size() else 0)
|
||||
borrow = 1 if value < 0 else 0
|
||||
result.append(value + BASE if borrow else value)
|
||||
return trim(result)
|
||||
|
||||
static func multiply(a: Array, b: Array) -> Array:
|
||||
var result: Array = []
|
||||
result.resize(a.size() + b.size())
|
||||
result.fill(0)
|
||||
for i in a.size():
|
||||
var carry: int = 0
|
||||
for j in b.size():
|
||||
# < BASE^2 + 2*BASE: comfortably inside signed int64.
|
||||
var value: int = a[i] * b[j] + result[i + j] + carry
|
||||
result[i + j] = value % BASE
|
||||
carry = value / BASE
|
||||
result[i + b.size()] = carry
|
||||
return trim(result)
|
||||
|
||||
static func divmod(a: Array, b: Array) -> Array:
|
||||
assert(b != [0], "Division by zero")
|
||||
if compare(a, b) < 0: return [[0], a.duplicate()]
|
||||
if b.size() == 1:
|
||||
var quotient: Array = []
|
||||
quotient.resize(a.size())
|
||||
var carry: int = 0
|
||||
for i in range(a.size() - 1, -1, -1):
|
||||
var value: int = carry * BASE + a[i]
|
||||
quotient[i] = value / b[0]
|
||||
carry = value % b[0]
|
||||
return [trim(quotient), [carry]]
|
||||
# Normalize the leading divisor limb so the two-limb quotient estimate
|
||||
# needs at most two downward corrections (Knuth's division estimate).
|
||||
var factor: int = BASE / (b.back() + 1)
|
||||
a = multiply(a, [factor])
|
||||
b = multiply(b, [factor])
|
||||
var quotient: Array = []
|
||||
quotient.resize(a.size())
|
||||
var remainder: Array = [0]
|
||||
for i in range(a.size() - 1, -1, -1):
|
||||
remainder.push_front(a[i])
|
||||
trim(remainder)
|
||||
var high: int = remainder[b.size()] if remainder.size() > b.size() else 0
|
||||
var low: int = remainder[b.size() - 1] if remainder.size() >= b.size() else 0
|
||||
var estimate: int = mini(BASE - 1, (high * BASE + low) / b.back())
|
||||
var product: Array = multiply(b, [estimate])
|
||||
while compare(product, remainder) > 0:
|
||||
estimate -= 1
|
||||
product = subtract(product, b)
|
||||
quotient[i] = estimate
|
||||
remainder = subtract(remainder, product)
|
||||
return [trim(quotient), divmod(remainder, [factor])[0]]
|
||||
|
||||
static func gcd(a: Array, b: Array) -> Array:
|
||||
while b != [0]:
|
||||
var remainder: Array = divmod(a, b)[1]
|
||||
a = b
|
||||
b = remainder
|
||||
return a
|
||||
@@ -0,0 +1 @@
|
||||
uid://6wfbh3vq5smd
|
||||
@@ -0,0 +1,322 @@
|
||||
extends RefCounted
|
||||
## Exact signed rational arithmetic. Native small integers are the fast path.
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
const Value = preload("res://world/owniverse/math/exact_value.gd")
|
||||
const SMALL: int = 1000000000
|
||||
const MAX_INT: int = 9223372036854775807
|
||||
const MIN_INT: int = -9223372036854775807 - 1
|
||||
const CARRY_SCALE: Array = [0, 0, 0, 0, 1] # 10^36
|
||||
|
||||
static func make(n: Array, d: Array = [1], polarity: int = 1):
|
||||
assert(d != [0], "Zero denominator")
|
||||
if n == [0]: return 0
|
||||
if d != [1]:
|
||||
var common: Array = Big.gcd(n, d)
|
||||
n = Big.divmod(n, common)[0]
|
||||
d = Big.divmod(d, common)[0]
|
||||
if d == [1] and (n.size() == 1 or (n.size() == 2 and n[1] < SMALL)):
|
||||
return polarity * (n[0] + (n[1] * SMALL if n.size() == 2 else 0))
|
||||
if d == [1] and n.size() == 3:
|
||||
var limit: Array = [854775807 if polarity > 0 else 854775808, 223372036, 9]
|
||||
if Big.compare(n, limit) <= 0:
|
||||
return (("-" if polarity < 0 else "") + Big.decimal(n)).to_int()
|
||||
var result = Value.new()
|
||||
result.numerator = n
|
||||
result.denominator = d
|
||||
result.polarity = polarity
|
||||
return result
|
||||
|
||||
static func of(value):
|
||||
if value is Value or value is int: return value
|
||||
if value is float:
|
||||
if not is_finite(value): return value
|
||||
if value == floorf(value) and absf(value) < 1e18: return int(value)
|
||||
# Imports the actual binary float, not an invented decimal rounding.
|
||||
var data := PackedByteArray()
|
||||
data.resize(8)
|
||||
data.encode_double(0, value)
|
||||
var bits: int = data.decode_s64(0)
|
||||
var exponent: int = (bits >> 52) & 2047
|
||||
var mantissa: int = bits & 4503599627370495
|
||||
if exponent: mantissa += 4503599627370496
|
||||
var power: int = exponent - 1075 if exponent else -1074
|
||||
var n: Array = Big.parse(str(mantissa))
|
||||
var d: Array = [1]
|
||||
for i in absi(power):
|
||||
if power > 0: n = Big.multiply(n, [2])
|
||||
else: d = Big.multiply(d, [2])
|
||||
return make(n, d, -1 if value < 0 else 1)
|
||||
var text: String = str(value).strip_edges().to_lower()
|
||||
if text == "": return 0
|
||||
if "/" in text:
|
||||
var terms = text.split("/")
|
||||
assert(terms.size() == 2)
|
||||
return divide(of(terms[0]), of(terms[1]))
|
||||
var polarity: int = -1 if text.begins_with("-") else 1
|
||||
text = text.trim_prefix("-").trim_prefix("+")
|
||||
var terms = text.split("e")
|
||||
var power: int = int(terms[1]) if terms.size() == 2 else 0
|
||||
text = terms[0]
|
||||
var point: int = text.find(".")
|
||||
if point >= 0: power -= text.length() - point - 1
|
||||
text = text.replace(".", "").replace("_", "")
|
||||
assert(text.is_valid_int(), "Invalid exact number: " + str(value))
|
||||
assert(absi(power) <= 10000, "Unreasonable decimal exponent")
|
||||
return make(Big.parse(text + "0".repeat(maxi(0, power))), Big.parse("1" + "0".repeat(maxi(0, -power))), polarity)
|
||||
|
||||
static func parts(value) -> Array:
|
||||
value = of(value)
|
||||
if value is int: return [Big.parse(str(value).trim_prefix("-")), [1], -1 if value < 0 else 1]
|
||||
return [value.numerator, value.denominator, value.polarity]
|
||||
|
||||
static func numeric(value) -> bool:
|
||||
return value is int or value is float or value is Value
|
||||
|
||||
static func finite(value) -> bool:
|
||||
return value is Value or value is int or (value is float and is_finite(value))
|
||||
|
||||
static func approx(value) -> float:
|
||||
if not value is Value: return float(value)
|
||||
var a: String = Big.decimal(value.numerator)
|
||||
var b: String = Big.decimal(value.denominator)
|
||||
return value.polarity * float(a.left(16)) / float(b.left(16)) * pow(10.0, maxi(0, a.length() - 16) - maxi(0, b.length() - 16))
|
||||
|
||||
static func compare(a, b) -> int:
|
||||
if a is int and b is int: return 0 if a == b else (1 if a > b else -1)
|
||||
if not finite(a) or not finite(b):
|
||||
var x: float = approx(a)
|
||||
var y: float = approx(b)
|
||||
return 0 if x == y else (1 if x > y else -1)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
if x[2] != y[2]: return 1 if x[2] > y[2] else -1
|
||||
return x[2] * Big.compare(Big.multiply(x[0], y[1]), Big.multiply(y[0], x[1]))
|
||||
|
||||
static func eq(a, b) -> bool:
|
||||
if a == null: return b == null
|
||||
if b == null: return false
|
||||
if numeric(a) and numeric(b):
|
||||
if (a is float and is_nan(a)) or (b is float and is_nan(b)): return false
|
||||
return compare(a, b) == 0
|
||||
if a is Dictionary and b is Dictionary:
|
||||
if a.size() != b.size(): return false
|
||||
for k in a:
|
||||
if not b.has(k) or not eq(a[k], b[k]): return false
|
||||
return true
|
||||
if a is Array and b is Array:
|
||||
if a.size() != b.size(): return false
|
||||
for i in a.size():
|
||||
if not eq(a[i], b[i]): return false
|
||||
return true
|
||||
if (a is String or a is StringName) and (b is String or b is StringName):
|
||||
return str(a) == str(b)
|
||||
if typeof(a) != typeof(b): return false
|
||||
return a == b
|
||||
|
||||
static func ne(a, b) -> bool: return not eq(a, b)
|
||||
static func lt(a, b) -> bool: return compare(a, b) < 0 if numeric(a) and numeric(b) else a < b
|
||||
static func le(a, b) -> bool: return compare(a, b) <= 0 if numeric(a) and numeric(b) else a <= b
|
||||
static func gt(a, b) -> bool: return compare(a, b) > 0 if numeric(a) and numeric(b) else a > b
|
||||
static func ge(a, b) -> bool: return compare(a, b) >= 0 if numeric(a) and numeric(b) else a >= b
|
||||
static func minimum(a, b): return a if le(a, b) else b
|
||||
static func maximum(a, b): return a if ge(a, b) else b
|
||||
static func clamp_value(a, low, high): return minimum(maximum(a, low), high)
|
||||
static func neg(a): return subtract(0, a)
|
||||
static func absolute(a): return neg(a) if lt(a, 0) else a
|
||||
|
||||
static func add(a, b):
|
||||
if not numeric(a) or not numeric(b): return a + b
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a <= MAX_INT - b) or (b < 0 and a >= MIN_INT - b): return a + b
|
||||
if not finite(a) or not finite(b): return approx(a) + approx(b)
|
||||
if a is int and a == 0: return of(b)
|
||||
if b is int and b == 0: return of(a)
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var common: Array = Big.gcd(x[1], y[1])
|
||||
var xd: Array = Big.divmod(x[1], common)[0]
|
||||
var yd: Array = Big.divmod(y[1], common)[0]
|
||||
var left: Array = Big.multiply(x[0], yd)
|
||||
var right: Array = Big.multiply(y[0], xd)
|
||||
var denominator: Array = Big.multiply(xd, y[1])
|
||||
if x[2] == y[2]: return make(Big.add(left, right), denominator, x[2])
|
||||
var order: int = Big.compare(left, right)
|
||||
return make(Big.subtract(left, right) if order >= 0 else Big.subtract(right, left), denominator, x[2] if order >= 0 else y[2])
|
||||
|
||||
static func subtract(a, b):
|
||||
if not numeric(a) or not numeric(b): return a - b
|
||||
if a is int and b is int:
|
||||
if (b >= 0 and a >= MIN_INT + b) or (b < 0 and a <= MAX_INT + b): return a - b
|
||||
if not finite(a) or not finite(b): return approx(a) - approx(b)
|
||||
if b is int and b == 0: return of(a)
|
||||
var y: Array = parts(b)
|
||||
return add(a, make(y[0], y[1], -y[2]))
|
||||
|
||||
static func multiply(a, b):
|
||||
if not numeric(a) or not numeric(b): return a * b
|
||||
if not finite(a) or not finite(b): return approx(a) * approx(b)
|
||||
if (a is int and a == 0) or (b is int and b == 0): return 0
|
||||
if a is int and a == 1: return of(b)
|
||||
if b is int and b == 1: return of(a)
|
||||
if a is int and b is int and a != MIN_INT and b != MIN_INT:
|
||||
if absi(a) <= MAX_INT / absi(b): return a * b
|
||||
var x: Array = parts(a)
|
||||
var y: Array = parts(b)
|
||||
var g1: Array = Big.gcd(x[0], y[1])
|
||||
var g2: Array = Big.gcd(y[0], x[1])
|
||||
return make(Big.multiply(Big.divmod(x[0], g1)[0], Big.divmod(y[0], g2)[0]), Big.multiply(Big.divmod(x[1], g2)[0], Big.divmod(y[1], g1)[0]), x[2] * y[2])
|
||||
|
||||
static func divide(a, b):
|
||||
if not numeric(a) or not numeric(b): return a / b
|
||||
if not finite(a) or not finite(b): return approx(a) / approx(b)
|
||||
if b is int and b == 1: return of(a)
|
||||
if a is int and b is int and b != 0 and not (a == MIN_INT and b == -1):
|
||||
if a % b == 0: return a / b
|
||||
var y: Array = parts(b)
|
||||
assert(y[0] != [0], "Exact division by zero")
|
||||
return multiply(a, make(y[1], y[0], y[2]))
|
||||
|
||||
static func floor_value(a):
|
||||
if not finite(a): return a
|
||||
if a is int: return a
|
||||
var x: Array = parts(a)
|
||||
var qr: Array = Big.divmod(x[0], x[1])
|
||||
if x[2] < 0 and qr[1] != [0]: qr[0] = Big.add(qr[0], [1])
|
||||
return make(qr[0], [1], x[2])
|
||||
|
||||
static func ceil_value(a): return neg(floor_value(neg(a)))
|
||||
static func round_value(a): return multiply(-1 if lt(a, 0) else 1, floor_value(add(absolute(a), of("0.5"))))
|
||||
static func remainder(a, b): return subtract(a, multiply(truncate(divide(a, b)), b))
|
||||
static func truncate(a): return ceil_value(a) if lt(a, 0) else floor_value(a)
|
||||
static func integer(a) -> int:
|
||||
var result = truncate(of(a))
|
||||
assert(result is int, "Value does not fit native int64")
|
||||
return result
|
||||
|
||||
## Bound persistent fractions, never integer quantities or recipe arithmetic.
|
||||
## Truncation towards zero changes a value by less than 10^-36.
|
||||
static func bounded(value):
|
||||
if not value is Value or Big.compare(value.denominator, CARRY_SCALE) <= 0:
|
||||
return value
|
||||
var units: Array = Big.divmod(Big.multiply(value.numerator, CARRY_SCALE), value.denominator)[0]
|
||||
return make(units, CARRY_SCALE, value.polarity)
|
||||
|
||||
## Only call on a newly owned state snapshot, never on a published old state.
|
||||
static func bound_state(value):
|
||||
if value is Value: return bounded(value)
|
||||
if value is Dictionary:
|
||||
for key in value: value[key] = bound_state(value[key])
|
||||
elif value is Array:
|
||||
for i in value.size(): value[i] = bound_state(value[i])
|
||||
return value
|
||||
|
||||
static func power(a, b):
|
||||
if finite(b) and eq(b, floor_value(b)) and le(absolute(b), 10000):
|
||||
var exponent: int = integer(absolute(b))
|
||||
var result = 1
|
||||
while exponent:
|
||||
if exponent % 2: result = multiply(result, a)
|
||||
exponent /= 2
|
||||
if exponent: a = multiply(a, a)
|
||||
return divide(1, result) if lt(b, 0) else result
|
||||
return coefficient(pow(approx(a), approx(b)))
|
||||
|
||||
static func coefficient(value: float):
|
||||
# Only transcendental/RNG boundaries are approximate: 12 decimal places.
|
||||
if not is_finite(value): return value
|
||||
return of(String.num(value, 12))
|
||||
|
||||
static func square_root(a): return coefficient(sqrt(approx(a)))
|
||||
static func logarithm(a): return coefficient(log(approx(a)))
|
||||
static func near(a, b) -> bool:
|
||||
return le(absolute(subtract(a, b)), multiply(of("0.00001"), maximum(1, maximum(absolute(a), absolute(b)))))
|
||||
|
||||
static func contains(container, value) -> bool:
|
||||
if container is Array:
|
||||
for item in container:
|
||||
if eq(item, value): return true
|
||||
return false
|
||||
return value in container
|
||||
|
||||
static func fixed(value, digits: int = 0) -> String:
|
||||
var scale = power(10, digits)
|
||||
var rounded = round_value(multiply(value, scale))
|
||||
var text: String = str(absolute(rounded))
|
||||
if digits:
|
||||
text = text.lpad(digits + 1, "0")
|
||||
text = text.left(text.length() - digits) + "." + text.right(digits)
|
||||
return ("-" if lt(rounded, 0) else "") + text
|
||||
|
||||
static func scientific(value, digits: int = 4) -> String:
|
||||
if eq(value, 0): return fixed(0, digits) + "E+0"
|
||||
var terms: Array = parts(absolute(value))
|
||||
var exponent: int = Big.decimal(terms[0]).length() - Big.decimal(terms[1]).length()
|
||||
if lt(absolute(value), power(10, exponent)): exponent -= 1
|
||||
var mantissa = divide(value, power(10, exponent))
|
||||
if ge(absolute(round_value(multiply(mantissa, power(10, digits)))), multiply(10, power(10, digits))):
|
||||
mantissa = divide(mantissa, 10)
|
||||
exponent += 1
|
||||
return fixed(mantissa, digits) + ("E+%d" if exponent >= 0 else "E%d") % exponent
|
||||
|
||||
static func grouped(value, digits: int = 0) -> String:
|
||||
if eq(value, 0): return ""
|
||||
if ge(absolute(value), 1000000000): return scientific(value, digits)
|
||||
var text: String = fixed(absolute(value), digits)
|
||||
var parts_text: PackedStringArray = text.split(".")
|
||||
var integer_text: String = parts_text[0]
|
||||
var result: String = ""
|
||||
while integer_text.length() > 3:
|
||||
result = " " + integer_text.right(3) + result
|
||||
integer_text = integer_text.left(integer_text.length() - 3)
|
||||
return ("-" if lt(value, 0) else "") + integer_text + result + ("." + parts_text[1] if parts_text.size() > 1 else "")
|
||||
|
||||
static func encode(value):
|
||||
if value is Value: return {"$exact": str(value)}
|
||||
if value is int and (value > 9007199254740991 or value < -9007199254740991):
|
||||
return {"$exact": str(value)}
|
||||
if value is Dictionary:
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = encode(value[k])
|
||||
return result
|
||||
if value is Array:
|
||||
var result: Array = []
|
||||
for item in value: result.append(encode(item))
|
||||
return result
|
||||
return value
|
||||
|
||||
static func decode(value):
|
||||
if value is Dictionary:
|
||||
if value.size() == 1 and value.has("$exact"): return of(value["$exact"])
|
||||
var result: Dictionary = {}
|
||||
for k in value: result[k] = decode(value[k])
|
||||
return result
|
||||
if value is Array:
|
||||
var result: Array = []
|
||||
for item in value: result.append(decode(item))
|
||||
return result
|
||||
return value
|
||||
|
||||
static func valid_encoded(value) -> bool:
|
||||
if value is Dictionary:
|
||||
if value.has("$exact"):
|
||||
if value.size() != 1 or not value["$exact"] is String: return false
|
||||
var text: String = value["$exact"]
|
||||
if text.length() > 4096: return false
|
||||
var terms: PackedStringArray = text.split("/")
|
||||
if terms.size() > 2: return false
|
||||
var n: String = terms[0].trim_prefix("-")
|
||||
if n.is_empty(): return false
|
||||
for ch in n:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if terms.size() == 2:
|
||||
if terms[1].is_empty(): return false
|
||||
for ch in terms[1]:
|
||||
if ch < "0" or ch > "9": return false
|
||||
if Big.parse(terms[1]) == [0]: return false
|
||||
return true
|
||||
for k in value:
|
||||
if not valid_encoded(value[k]): return false
|
||||
if value is Array:
|
||||
for item in value:
|
||||
if not valid_encoded(item): return false
|
||||
return true
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfxfoprykygry
|
||||
@@ -0,0 +1,9 @@
|
||||
extends RefCounted
|
||||
## Immutable by contract; arithmetic creates new values. Signed reduced fraction.
|
||||
var polarity: int = 1
|
||||
var numerator: Array = [0]
|
||||
var denominator: Array = [1]
|
||||
|
||||
func _to_string() -> String:
|
||||
const Big = preload("res://world/owniverse/math/big_integer.gd")
|
||||
return ("-" if polarity < 0 else "") + Big.decimal(numerator) + ("/" + Big.decimal(denominator) if denominator != [1] else "")
|
||||
@@ -0,0 +1 @@
|
||||
uid://qv2rdp4s0d36
|
||||
+105
-117
@@ -1,181 +1,169 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
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")
|
||||
|
||||
func scale_batches(target: Dictionary, id: String, factor: float) -> void:
|
||||
func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
|
||||
var batches: Array = target.batches.get(id, [])
|
||||
var total: float = 0.0
|
||||
var assigned: float = 0.0
|
||||
var total: Variant = 0
|
||||
var assigned: Variant = 0
|
||||
var shares: Array = []
|
||||
for batch : Dictionary in batches:
|
||||
var expected: float = batch.amount * maxf(0.0, factor)
|
||||
total += expected
|
||||
batch.amount = floorf(expected)
|
||||
assigned += batch.amount
|
||||
shares.append({"batch": batch, "rest": expected - batch.amount})
|
||||
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.rest > b.rest)
|
||||
var expected: Variant = E.multiply(batch.amount, E.maximum(0, factor))
|
||||
total = E.add(total, expected)
|
||||
batch.amount = E.floor_value(expected)
|
||||
assigned = E.add(assigned, batch.amount)
|
||||
shares.append({"batch": batch, "rest": E.subtract(expected, batch.amount)})
|
||||
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.gt(a.rest, b.rest))
|
||||
for share : Dictionary in shares:
|
||||
if assigned < roundf(total):
|
||||
share.batch.amount += 1.0
|
||||
assigned += 1.0
|
||||
share.batch.withdraw_remainder = minf(share.batch.get("withdraw_remainder", 0.0), share.batch.amount)
|
||||
if E.lt(assigned, E.round_value(total)):
|
||||
share.batch.amount = E.add(share.batch.amount, 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount)
|
||||
|
||||
|
||||
func append_batch(target: Dictionary, id: String, amount: float,
|
||||
created_from: float, created_to: float, completed: Dictionary = {},
|
||||
completed_before: float = -INF) -> float:
|
||||
if amount <= 0.0 or not world.catalog.entities.has(id) or world.catalog.entities[id].lifespan <= 0.0:
|
||||
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF) -> Variant:
|
||||
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
|
||||
return NAN
|
||||
if created_to < created_from:
|
||||
if E.lt(created_to, created_from):
|
||||
return NAN
|
||||
if not target.batches.has(id):
|
||||
if (not target.batches.has(id)):
|
||||
target.batches[id] = []
|
||||
if not target.has("batch_origins"):
|
||||
if (not target.has("batch_origins")):
|
||||
target.batch_origins = {}
|
||||
var batches: Array = target.batches[id]
|
||||
var lifespan: float = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
|
||||
if world.catalog.entities[id].group_as == "Star":
|
||||
lifespan /= WorldParameters.effective(target, "Bur")
|
||||
var batch_duration: float = lifespan / Global.batch_count if lifespan > 10.0 else lifespan
|
||||
if batch_duration <= 0.0:
|
||||
var lifespan: Variant = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
|
||||
if E.eq(world.catalog.entities[id].group_as, "Star"):
|
||||
lifespan = E.divide(lifespan, WorldParameters.effective(target, "Bur"))
|
||||
var batch_duration: Variant = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
|
||||
if E.le(batch_duration, 0):
|
||||
return NAN
|
||||
if not target.batch_origins.has(id):
|
||||
target.batch_origins[id] = batches[0].get("created_from", created_from) if not batches.is_empty() else created_from
|
||||
var origin: float = target.batch_origins[id]
|
||||
var cursor: float = created_from
|
||||
var duration: float = created_to - created_from
|
||||
var remaining_amount: float = amount
|
||||
var completed_amount: float = 0.0
|
||||
var completed_from: float = INF
|
||||
var completed_to: float = -INF
|
||||
# Subtract virtual windows in their original order to preserve floating-point
|
||||
# rounding of lifetime production, rather than subtracting one large sum.
|
||||
var remaining_object_years: float = amount * maxf(0.0, completed_before - (created_from + created_to) * 0.5) if is_finite(completed_before) else NAN
|
||||
# Compute the index once. Recomputing floor from a rounded boundary can
|
||||
# select the preceding window forever (e.g. fractional origins at large ages).
|
||||
var window_index: float = floorf((cursor - origin) / batch_duration)
|
||||
if origin + (window_index + 1.0) * batch_duration <= cursor:
|
||||
window_index += 1.0
|
||||
if (not target.batch_origins.has(id)):
|
||||
target.batch_origins[id] = (batches[0].get("created_from", created_from) if (not batches.is_empty()) else created_from)
|
||||
var origin: Variant = target.batch_origins[id]
|
||||
var cursor: Variant = created_from
|
||||
var duration: Variant = E.subtract(created_to, created_from)
|
||||
var remaining_amount: Variant = amount
|
||||
var completed_amount: Variant = 0
|
||||
var completed_from: Variant = INF
|
||||
var completed_to: Variant = E.neg(INF)
|
||||
# Subtract each completed window's lifetime production exactly.
|
||||
var remaining_object_years: Variant = (E.multiply(amount, E.maximum(0, E.subtract(completed_before, E.multiply((E.add(created_from, created_to)), E.of("0.5"))))) if E.finite(completed_before) else NAN)
|
||||
# Advance an exact window index rather than repeatedly finding a boundary.
|
||||
var window_index: Variant = E.floor_value(E.divide((E.subtract(cursor, origin)), batch_duration))
|
||||
if E.le(E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration)), cursor):
|
||||
window_index = E.add(window_index, 1)
|
||||
var batches_by_start: Dictionary = {}
|
||||
for existing : Dictionary in batches:
|
||||
var start: float = existing.get("created_from", -INF)
|
||||
if not batches_by_start.has(start):
|
||||
batches_by_start[start] = []
|
||||
batches_by_start[start].append(existing)
|
||||
var start: Variant = existing.get("created_from", E.neg(INF))
|
||||
if (not batches_by_start.has(str(start))):
|
||||
batches_by_start[str(start)] = []
|
||||
batches_by_start[str(start)].append(existing)
|
||||
while true:
|
||||
var window_start: float = origin + window_index * batch_duration
|
||||
var window_end: float = origin + (window_index + 1.0) * batch_duration
|
||||
if window_end <= cursor:
|
||||
push_error("Batch window exceeds floating-point time precision: " + id)
|
||||
var window_start: Variant = E.add(origin, E.multiply(window_index, batch_duration))
|
||||
var window_end: Variant = E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration))
|
||||
if E.le(window_end, cursor):
|
||||
push_error("Batch window does not advance: " + id)
|
||||
return remaining_object_years
|
||||
var part_end: float = minf(created_to, window_end)
|
||||
var part_amount: float = remaining_amount
|
||||
if duration > 0.0 and part_end < created_to:
|
||||
part_amount = floorf(amount * (part_end - created_from) / duration) - (amount - remaining_amount)
|
||||
if window_end + lifespan <= completed_before:
|
||||
var part_end: Variant = E.minimum(created_to, window_end)
|
||||
var part_amount: Variant = remaining_amount
|
||||
if (E.gt(duration, 0) and E.lt(part_end, created_to)):
|
||||
part_amount = E.subtract(E.floor_value(E.divide(E.multiply(amount, (E.subtract(part_end, created_from))), duration)), (E.subtract(amount, remaining_amount)))
|
||||
if E.le(E.add(window_end, lifespan), completed_before):
|
||||
# Retain the original integer allocation and lifetime integral, but
|
||||
# do not allocate thousands of already-dead batch dictionaries.
|
||||
completed_amount += part_amount
|
||||
completed_from = minf(completed_from, window_start + lifespan)
|
||||
completed_to = maxf(completed_to, window_end + lifespan)
|
||||
remaining_object_years -= part_amount * expired_years(completed_before, window_start + lifespan, window_end + lifespan)
|
||||
completed_amount = E.add(completed_amount, part_amount)
|
||||
completed_from = E.minimum(completed_from, E.add(window_start, lifespan))
|
||||
completed_to = E.maximum(completed_to, E.add(window_end, lifespan))
|
||||
remaining_object_years = E.subtract(remaining_object_years, E.multiply(part_amount, expired_years(completed_before, E.add(window_start, lifespan), E.add(window_end, lifespan))))
|
||||
else:
|
||||
var batch: Dictionary = {}
|
||||
for existing: Dictionary in batches_by_start.get(window_start, []):
|
||||
if existing.get("created_from", -INF) == window_start and existing.start_year == window_start + lifespan and existing.end_year == window_end + lifespan:
|
||||
for existing : Dictionary in batches_by_start.get(str(window_start), []):
|
||||
if ((E.eq(existing.get("created_from", E.neg(INF)), window_start) and E.eq(existing.start_year, E.add(window_start, lifespan))) and E.eq(existing.end_year, E.add(window_end, lifespan))):
|
||||
batch = existing
|
||||
break
|
||||
if batch.is_empty():
|
||||
batch = {
|
||||
"created_from": window_start,
|
||||
"created_to": window_end,
|
||||
"start_year": window_start + lifespan,
|
||||
"end_year": window_end + lifespan,
|
||||
"amount": 0.0,
|
||||
}
|
||||
batch = {"created_from": window_start, "created_to": window_end, "start_year": E.add(window_start, lifespan), "end_year": E.add(window_end, lifespan), "amount": 0}
|
||||
batches.append(batch)
|
||||
if not batches_by_start.has(window_start):
|
||||
batches_by_start[window_start] = []
|
||||
batches_by_start[window_start].append(batch)
|
||||
batch.amount += part_amount
|
||||
remaining_amount -= part_amount
|
||||
if duration == 0.0 or part_end >= created_to:
|
||||
if (not batches_by_start.has(str(window_start))):
|
||||
batches_by_start[str(window_start)] = []
|
||||
batches_by_start[str(window_start)].append(batch)
|
||||
batch.amount = E.add(batch.amount, part_amount)
|
||||
remaining_amount = E.subtract(remaining_amount, part_amount)
|
||||
if (E.eq(duration, 0) or E.ge(part_end, created_to)):
|
||||
break
|
||||
cursor = window_end
|
||||
window_index += 1.0
|
||||
batches.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.created_from < b.created_from)
|
||||
if completed_amount > 0.0:
|
||||
if not completed.has(id): completed[id] = []
|
||||
window_index = E.add(window_index, 1)
|
||||
batches.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.lt(a.created_from, b.created_from))
|
||||
if E.gt(completed_amount, 0):
|
||||
if (not completed.has(id)): completed[id] = []
|
||||
completed[id].append({"amount": completed_amount, "from": completed_from, "to": completed_to})
|
||||
return remaining_object_years
|
||||
|
||||
|
||||
func withdraw_batches(target: Dictionary, id: String, to_year: float) -> Array:
|
||||
func withdraw_batches(target: Dictionary, id: String, to_year: Variant) -> Array:
|
||||
var expired: Array = []
|
||||
var remaining: Array = []
|
||||
for batch : Dictionary in target.batches.get(id, []):
|
||||
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: float = batch.end_year
|
||||
var amount: float = batch.amount
|
||||
if amount <= 0.0:
|
||||
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: Variant = batch.end_year
|
||||
var amount: Variant = batch.amount
|
||||
if E.le(amount, 0):
|
||||
continue
|
||||
if to_year < start or (to_year == start and end > start):
|
||||
if (E.lt(to_year, start) or ((E.eq(to_year, start) and E.gt(end, start)))):
|
||||
remaining.append(batch)
|
||||
continue
|
||||
var consumed: float = amount
|
||||
var until: float = minf(to_year, end)
|
||||
if to_year < end:
|
||||
var carry: float = batch.get("withdraw_remainder", 0.0)
|
||||
var expected: float = carry + (amount - carry) * (to_year - start) / (end - start)
|
||||
consumed = floorf(expected)
|
||||
batch.withdraw_remainder = expected - consumed
|
||||
batch.amount = maxf(0.0, amount - consumed)
|
||||
var consumed: Variant = amount
|
||||
var until: Variant = E.minimum(to_year, end)
|
||||
if E.lt(to_year, end):
|
||||
var carry: Variant = batch.get("withdraw_remainder", 0)
|
||||
var expected: Variant = E.add(carry, E.divide(E.multiply((E.subtract(amount, carry)), (E.subtract(to_year, start))), (E.subtract(end, start))))
|
||||
consumed = E.floor_value(expected)
|
||||
batch.withdraw_remainder = E.subtract(expected, consumed)
|
||||
batch.amount = E.maximum(0, E.subtract(amount, consumed))
|
||||
batch.processed_until = to_year
|
||||
remaining.append(batch)
|
||||
if consumed > 0.0:
|
||||
if E.gt(consumed, 0):
|
||||
expired.append({"amount": consumed, "from": start, "to": until})
|
||||
target.batches[id] = remaining
|
||||
return expired
|
||||
|
||||
|
||||
func object_years(source: Dictionary, id: String, quantity: float,
|
||||
from_year: float, to_year: float) -> float:
|
||||
var result: float = quantity * SimulationTime.elapsed_years(from_year, to_year)
|
||||
func object_years(source: Dictionary, id: String, quantity: Variant, from_year: Variant, to_year: Variant) -> Variant:
|
||||
var result: Variant = E.multiply(quantity, SimulationTime.elapsed_years(from_year, to_year))
|
||||
for batch : Dictionary in source.batches.get(id, []):
|
||||
var start: float = maxf(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: float = batch.end_year
|
||||
result -= batch.amount * (expired_years(to_year, start, end) - expired_years(from_year, start, end))
|
||||
return maxf(0.0, result)
|
||||
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
|
||||
var end: Variant = batch.end_year
|
||||
result = E.subtract(result, E.multiply(batch.amount, (E.subtract(expired_years(to_year, start, end), expired_years(from_year, start, end)))))
|
||||
return E.maximum(0, result)
|
||||
|
||||
|
||||
func expired_years(year: float, start: float, end: float) -> float:
|
||||
if year <= start:
|
||||
return 0.0
|
||||
if end <= start:
|
||||
return year - start
|
||||
if year >= end:
|
||||
return year - end + (end - start) * 0.5
|
||||
return (year - start) * (year - start) / (2.0 * (end - start))
|
||||
func expired_years(year: Variant, start: Variant, end: Variant) -> Variant:
|
||||
if E.le(year, start):
|
||||
return 0
|
||||
if E.le(end, start):
|
||||
return E.subtract(year, start)
|
||||
if E.ge(year, end):
|
||||
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5")))
|
||||
return E.divide(E.multiply((E.subtract(year, start)), (E.subtract(year, start))), (E.multiply(2, (E.subtract(end, start)))))
|
||||
|
||||
|
||||
func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float:
|
||||
var total: float = 0.0
|
||||
func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
|
||||
var total: Variant = 0
|
||||
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||
for part : Dictionary in withdraw_batches(world.state, entity.id, current_year):
|
||||
total += part.amount
|
||||
var fraction: float = clampf(total / physical[entity.id], 0.0, 1.0) if physical.get(entity.id, 0.0) > 0.0 else 0.0
|
||||
world.state.amount[entity.id] = world.state.amount.get(entity.id, 0.0) * (1.0 - fraction)
|
||||
total = E.add(total, part.amount)
|
||||
var fraction: Variant = (E.clamp_value(E.divide(total, physical[entity.id]), 0, 1) if E.gt(physical.get(entity.id, 0), 0) else 0)
|
||||
world.state.amount[entity.id] = E.multiply(world.state.amount.get(entity.id, 0), (E.subtract(1, fraction)))
|
||||
for owner_id in world.state.binds:
|
||||
if world.state.binds[owner_id].has(entity.id):
|
||||
world.state.binds[owner_id][entity.id] *= 1.0 - fraction
|
||||
world.state.binds[owner_id][entity.id] = E.multiply(world.state.binds[owner_id][entity.id], E.subtract(1, fraction))
|
||||
world.ledger.round_object_allocations(world.state)
|
||||
world.ledger.refresh_groups(world.state)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
return total
|
||||
|
||||
|
||||
func add_entity_to_batch(entity_id: String, amount: float, current_year: float) -> void:
|
||||
func add_entity_to_batch(entity_id: String, amount: Variant, current_year: Variant) -> void:
|
||||
append_batch(world.state, entity_id, amount, current_year, current_year)
|
||||
|
||||
+109
-116
@@ -1,23 +1,24 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
func physical_amounts(source: Dictionary) -> Dictionary:
|
||||
var amounts: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if not world.catalog.is_group(id):
|
||||
amounts[id] = maxf(0.0, source.amount.get(id, 0.0))
|
||||
if (not world.catalog.is_group(id)):
|
||||
amounts[id] = E.maximum(0, source.amount.get(id, 0))
|
||||
for bindings : Dictionary in source.binds.values():
|
||||
for id in bindings:
|
||||
if amounts.has(id):
|
||||
amounts[id] += maxf(0.0, bindings[id])
|
||||
amounts[id] = E.add(amounts[id], E.maximum(0, bindings[id]))
|
||||
return amounts
|
||||
|
||||
|
||||
func available(source: Dictionary, id: String) -> float:
|
||||
var amount: float = 0.0
|
||||
func available(source: Dictionary, id: String) -> Variant:
|
||||
var amount: Variant = 0
|
||||
for member in world.catalog.members(id):
|
||||
amount += maxf(0.0, source.amount.get(member, 0.0))
|
||||
amount = E.add(amount, E.maximum(0, source.amount.get(member, 0)))
|
||||
return amount
|
||||
|
||||
|
||||
@@ -26,42 +27,41 @@ func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictio
|
||||
for id in requirements:
|
||||
var weights: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
weights[member] = source.amount.get(member, 0.0)
|
||||
weights[member] = source.amount.get(member, 0)
|
||||
weights = QuantityMath.normalize_distribution(weights)
|
||||
for member in weights:
|
||||
expanded[member] = expanded.get(member, 0.0) + maxf(0.0, requirements[id]) * weights[member]
|
||||
expanded[member] = E.add(expanded.get(member, 0), E.multiply(E.maximum(0, requirements[id]), weights[member]))
|
||||
return expanded
|
||||
|
||||
|
||||
func take_available(target: Dictionary, id: String, amount: float) -> Dictionary:
|
||||
func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary:
|
||||
var quantities: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
quantities[member] = target.amount.get(member, 0.0)
|
||||
quantities[member] = target.amount.get(member, 0)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
var taken: Dictionary = {}
|
||||
var total: float = minf(amount, available(target, id))
|
||||
var discrete: bool = not distribution.is_empty()
|
||||
var total: Variant = E.minimum(amount, available(target, id))
|
||||
var discrete: bool = (not distribution.is_empty())
|
||||
for member in distribution:
|
||||
discrete = discrete and is_discrete(member)
|
||||
discrete = (discrete and is_discrete(member))
|
||||
if discrete:
|
||||
total = floorf(total)
|
||||
total = E.floor_value(total)
|
||||
var shares: Array = []
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 0
|
||||
for member in distribution:
|
||||
var expected: float = total * distribution[member]
|
||||
taken[member] = floorf(expected) if discrete else expected
|
||||
assigned += taken[member]
|
||||
shares.append({"id": member, "rest": expected - taken[member]})
|
||||
var expected: Variant = E.multiply(total, distribution[member])
|
||||
taken[member] = (E.floor_value(expected) if discrete else expected)
|
||||
assigned = E.add(assigned, taken[member])
|
||||
shares.append({"id": member, "rest": E.subtract(expected, taken[member])})
|
||||
if discrete:
|
||||
shares.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))
|
||||
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for share : Dictionary in shares:
|
||||
if assigned >= total:
|
||||
if E.ge(assigned, total):
|
||||
break
|
||||
taken[share.id] += 1.0
|
||||
assigned += 1.0
|
||||
taken[share.id] = E.add(taken[share.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
for member in taken:
|
||||
target.amount[member] = maxf(0.0, target.amount.get(member, 0.0) - taken[member])
|
||||
target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member]))
|
||||
return taken
|
||||
|
||||
|
||||
@@ -69,45 +69,44 @@ func is_discrete(_id: String) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func credit(target: Dictionary, id: String, amount: float) -> float:
|
||||
if amount <= 0.0:
|
||||
return 0.0
|
||||
func credit(target: Dictionary, id: String, amount: Variant) -> Variant:
|
||||
if E.le(amount, 0):
|
||||
return 0
|
||||
if is_discrete(id):
|
||||
if not target.has("production_remainders"):
|
||||
if (not target.has("production_remainders")):
|
||||
target.production_remainders = {}
|
||||
var expected: float = amount + target.production_remainders.get(id, 0.0)
|
||||
amount = floorf(expected)
|
||||
target.production_remainders[id] = expected - amount
|
||||
target.amount[id] = target.amount.get(id, 0.0) + amount
|
||||
target.stats.total_amount[id] = target.stats.total_amount.get(id, 0.0) + amount
|
||||
if world.catalog.entities.has(id) and world.catalog.entities[id].group_as != "" and world.catalog.entities[id].group_as != id:
|
||||
var expected: Variant = E.add(amount, target.production_remainders.get(id, 0))
|
||||
amount = E.floor_value(expected)
|
||||
target.production_remainders[id] = E.subtract(expected, amount)
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), amount)
|
||||
target.stats.total_amount[id] = E.add(target.stats.total_amount.get(id, 0), amount)
|
||||
if ((world.catalog.entities.has(id) and E.ne(world.catalog.entities[id].group_as, "")) and E.ne(world.catalog.entities[id].group_as, id)):
|
||||
var group: String = world.catalog.entities[id].group_as
|
||||
target.stats.total_amount[group] = target.stats.total_amount.get(group, 0.0) + amount
|
||||
target.stats.total_amount[group] = E.add(target.stats.total_amount.get(group, 0), amount)
|
||||
return amount
|
||||
|
||||
|
||||
func round_object_allocations(target: Dictionary) -> void:
|
||||
var physical: Dictionary = physical_amounts(target)
|
||||
for id in physical:
|
||||
if not is_discrete(id):
|
||||
if (not is_discrete(id)):
|
||||
continue
|
||||
var slots: Array = [ {"owner": "", "value": target.amount.get(id, 0.0)}]
|
||||
var slots: Array = [{"owner": "", "value": target.amount.get(id, 0)}]
|
||||
for owner_id in target.binds:
|
||||
if target.binds[owner_id].has(id):
|
||||
slots.append({"owner": owner_id, "value": target.binds[owner_id][id]})
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 0
|
||||
for slot : Dictionary in slots:
|
||||
slot.whole = floorf(slot.value)
|
||||
slot.rest = slot.value - slot.whole
|
||||
assigned += slot.whole
|
||||
slots.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return a.rest > b.rest if a.rest != b.rest else str(a.owner) < str(b.owner))
|
||||
var left: float = roundf(physical[id]) - assigned
|
||||
slot.whole = E.floor_value(slot.value)
|
||||
slot.rest = E.subtract(slot.value, slot.whole)
|
||||
assigned = E.add(assigned, slot.whole)
|
||||
slots.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.owner), str(b.owner))))
|
||||
var left: Variant = E.subtract(E.round_value(physical[id]), assigned)
|
||||
for slot : Dictionary in slots:
|
||||
if left > 0.0:
|
||||
slot.whole += 1.0
|
||||
left -= 1.0
|
||||
if slot.owner == "":
|
||||
if E.gt(left, 0):
|
||||
slot.whole = E.add(slot.whole, 1)
|
||||
left = E.subtract(left, 1)
|
||||
if E.eq(slot.owner, ""):
|
||||
target.amount[id] = slot.whole
|
||||
else:
|
||||
target.binds[slot.owner][id] = slot.whole
|
||||
@@ -117,48 +116,42 @@ func refresh_groups(target: Dictionary) -> void:
|
||||
var groups: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
var group: String = world.catalog.entities[id].group_as
|
||||
if group != "":
|
||||
groups[group] = groups.get(group, 0.0) + target.amount.get(id, 0.0)
|
||||
if E.ne(group, ""):
|
||||
groups[group] = E.add(groups.get(group, 0), target.amount.get(id, 0))
|
||||
for group in groups:
|
||||
target.amount[group] = groups[group]
|
||||
|
||||
|
||||
func clamp_auto_resources(target: Dictionary) -> void:
|
||||
for id in target.get("auto_resources", {}):
|
||||
target.auto_resources[id] = minf(target.auto_resources[id], available(target, id))
|
||||
target.auto_resources[id] = E.minimum(target.auto_resources[id], available(target, id))
|
||||
|
||||
|
||||
func bind_full(target: Dictionary, amounts: Dictionary) -> bool:
|
||||
# Binding only mutates free amounts and owners' holdings.
|
||||
var trial: Dictionary = {
|
||||
"amount": target.amount.duplicate(),
|
||||
"binds": target.binds.duplicate(true),
|
||||
}
|
||||
var trial: Dictionary = {"amount": target.amount.duplicate(), "binds": target.binds.duplicate(true)}
|
||||
var requests: Array = []
|
||||
for owner_id in amounts:
|
||||
if not world.catalog.entities.has(owner_id):
|
||||
if (not world.catalog.entities.has(owner_id)):
|
||||
return false
|
||||
for id in world.catalog.entities[owner_id].bind:
|
||||
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
|
||||
if not is_equal_approx(quantity, roundf(quantity)):
|
||||
var quantity: Variant = E.multiply(amounts[owner_id], E.maximum(0, world.catalog.entities[owner_id].bind[id]))
|
||||
if (not E.eq(quantity, E.round_value(quantity))):
|
||||
return false
|
||||
if quantity > 0.0:
|
||||
requests.append({"owner": owner_id, "id": id, "amount": roundf(quantity), "members": world.catalog.members(id).size()})
|
||||
requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
if a.members != b.members:
|
||||
return a.members < b.members
|
||||
return str(a.id) < str(b.id) if a.id != b.id else str(a.owner) < str(b.owner))
|
||||
if E.gt(quantity, 0):
|
||||
requests.append({"owner": owner_id, "id": id, "amount": E.round_value(quantity), "members": world.catalog.members(id).size()})
|
||||
requests.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.lt(a.members, b.members) if E.ne(a.members, b.members) else ((E.lt(str(a.id), str(b.id)) if E.ne(a.id, b.id) else E.lt(str(a.owner), str(b.owner))))))
|
||||
for request : Dictionary in requests:
|
||||
var taken: Dictionary = take_available(trial, request.id, request.amount)
|
||||
var total: float = 0.0
|
||||
var total: Variant = 0
|
||||
for quantity in taken.values():
|
||||
total += quantity
|
||||
if total != request.amount:
|
||||
total = E.add(total, quantity)
|
||||
if E.ne(total, request.amount):
|
||||
return false
|
||||
if not trial.binds.has(request.owner):
|
||||
if (not trial.binds.has(request.owner)):
|
||||
trial.binds[request.owner] = {}
|
||||
for id in taken:
|
||||
trial.binds[request.owner][id] = trial.binds[request.owner].get(id, 0.0) + taken[id]
|
||||
trial.binds[request.owner][id] = E.add(trial.binds[request.owner].get(id, 0), taken[id])
|
||||
target.amount = trial.amount
|
||||
target.binds = trial.binds
|
||||
return true
|
||||
@@ -169,51 +162,51 @@ func bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: bool
|
||||
return bind_full(target, amounts)
|
||||
var requests: Dictionary = {}
|
||||
for owner_id in amounts:
|
||||
if not world.catalog.entities.has(owner_id):
|
||||
if (not world.catalog.entities.has(owner_id)):
|
||||
continue
|
||||
for id in world.catalog.entities[owner_id].bind:
|
||||
var quantity: float = amounts[owner_id] * maxf(0.0, world.catalog.entities[owner_id].bind[id])
|
||||
var quantity: Variant = E.multiply(amounts[owner_id], E.maximum(0, world.catalog.entities[owner_id].bind[id]))
|
||||
var weights: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
weights[member] = target.amount.get(member, 0.0)
|
||||
weights[member] = target.amount.get(member, 0)
|
||||
weights = QuantityMath.normalize_distribution(weights)
|
||||
for member in weights:
|
||||
if not requests.has(member):
|
||||
if (not requests.has(member)):
|
||||
requests[member] = {}
|
||||
requests[member][owner_id] = requests[member].get(owner_id, 0.0) + quantity * weights[member]
|
||||
requests[member][owner_id] = E.add(requests[member].get(owner_id, 0), E.multiply(quantity, weights[member]))
|
||||
for id in requests:
|
||||
var requested: float = 0.0
|
||||
var requested: Variant = 0
|
||||
for quantity in requests[id].values():
|
||||
requested += quantity
|
||||
var available: float = available(target, id)
|
||||
var factor: float = minf(1.0, available / requested) if requested > 0.0 else 0.0
|
||||
requested = E.add(requested, quantity)
|
||||
var available: Variant = available(target, id)
|
||||
var factor: Variant = (E.minimum(1, E.divide(available, requested)) if E.gt(requested, 0) else 0)
|
||||
for owner_id in requests[id]:
|
||||
var quantity: float = requests[id][owner_id] * factor
|
||||
if not target.binds.has(owner_id):
|
||||
var quantity: Variant = E.multiply(requests[id][owner_id], factor)
|
||||
if (not target.binds.has(owner_id)):
|
||||
target.binds[owner_id] = {}
|
||||
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + quantity
|
||||
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity)
|
||||
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), quantity)
|
||||
target.amount[id] = E.maximum(0, E.subtract(target.amount.get(id, 0), quantity))
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func apply_costs(entity: OwniverseEntity, amount: float) -> void:
|
||||
func apply_costs(entity: OwniverseEntity, amount: Variant) -> void:
|
||||
var costs: Dictionary = {}
|
||||
for resource in entity.cost:
|
||||
costs[resource] = roundf(maxf(0.0, entity.cost[resource]) * amount)
|
||||
costs[resource] = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
|
||||
for resource in costs:
|
||||
var quantities: Dictionary = physical_amounts(world.state)
|
||||
var taken: Dictionary = take_available(world.state, resource, costs[resource])
|
||||
for id in taken:
|
||||
var factor: float = 1.0 - taken[id] / quantities[id] if quantities.get(id, 0.0) > 0.0 else 0.0
|
||||
var factor: Variant = (E.subtract(1, E.divide(taken[id], quantities[id])) if E.gt(quantities.get(id, 0), 0) else 0)
|
||||
world.batches.scale_batches(world.state, id, factor)
|
||||
refresh_groups(world.state)
|
||||
clamp_auto_resources(world.state)
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bool:
|
||||
if not bind_new_owners(world.state, {item_id: amount}, true):
|
||||
func apply_binds(item_id: String, _entity: OwniverseEntity, amount: Variant) -> bool:
|
||||
if (not bind_new_owners(world.state, {item_id: amount}, true)):
|
||||
return false
|
||||
refresh_groups(world.state)
|
||||
clamp_auto_resources(world.state)
|
||||
@@ -222,20 +215,20 @@ func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bo
|
||||
return true
|
||||
|
||||
|
||||
func process_entity(entity_id: String, amount: float) -> void:
|
||||
if amount == 0:
|
||||
func process_entity(entity_id: String, amount: Variant) -> void:
|
||||
if E.eq(amount, 0):
|
||||
return
|
||||
|
||||
# Update state amount
|
||||
world.state.amount[entity_id] = world.state.amount.get(entity_id, 0.0) + amount
|
||||
world.state.stats.total_amount[entity_id] = world.state.stats.total_amount.get(entity_id, 0.0) + maxf(0.0, amount)
|
||||
world.state.amount[entity_id] = E.add(world.state.amount.get(entity_id, 0), amount)
|
||||
world.state.stats.total_amount[entity_id] = E.add(world.state.stats.total_amount.get(entity_id, 0), E.maximum(0, amount))
|
||||
|
||||
|
||||
# Handle group updates if entity belongs to a group
|
||||
var entity = world.catalog.entities[entity_id]
|
||||
if entity.group_as != "":
|
||||
world.state.amount[entity.group_as] = world.state.amount.get(entity.group_as, 0.0) + amount
|
||||
world.state.stats.total_amount[entity.group_as] = world.state.stats.total_amount.get(entity.group_as, 0.0) + maxf(0.0, amount)
|
||||
if E.ne(entity.group_as, ""):
|
||||
world.state.amount[entity.group_as] = E.add(world.state.amount.get(entity.group_as, 0), amount)
|
||||
world.state.stats.total_amount[entity.group_as] = E.add(world.state.stats.total_amount.get(entity.group_as, 0), E.maximum(0, amount))
|
||||
world.presentation.refresh(world.state, world.catalog)
|
||||
|
||||
|
||||
@@ -245,33 +238,33 @@ func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dict
|
||||
var bindings: Dictionary = target.binds.duplicate(true)
|
||||
var incoming: Dictionary = {}
|
||||
for owner_id in bindings:
|
||||
var owner_survival: float = 1.0 - fractions.get(owner_id, 0.0)
|
||||
var owner_survival: Variant = E.subtract(1, fractions.get(owner_id, 0))
|
||||
target.binds[owner_id] = {}
|
||||
for id in bindings[owner_id]:
|
||||
var held: float = bindings[owner_id][id]
|
||||
var released: float = held * (1.0 - owner_survival)
|
||||
target.amount[id] = target.amount.get(id, 0.0) + released
|
||||
var heirs: Dictionary = world.stellar.stellar_successors(outputs.get(owner_id, {})) if world.catalog.entities.has(id) and world.catalog.entities[id].group_as == "Planet" else {}
|
||||
var fraction: float = fractions.get(id, 0.0)
|
||||
var held: Variant = bindings[owner_id][id]
|
||||
var released: Variant = E.multiply(held, (E.subtract(1, owner_survival)))
|
||||
target.amount[id] = E.add(target.amount.get(id, 0), released)
|
||||
var heirs: Dictionary = (world.stellar.stellar_successors(outputs.get(owner_id, {})) if (world.catalog.entities.has(id) and E.eq(world.catalog.entities[id].group_as, "Planet")) else {})
|
||||
var fraction: Variant = fractions.get(id, 0)
|
||||
for heir in heirs:
|
||||
if not incoming.has(heir):
|
||||
if (not incoming.has(heir)):
|
||||
incoming[heir] = {}
|
||||
incoming[heir][id] = incoming[heir].get(id, 0.0) + released * (1.0 - fraction) * heirs[heir]
|
||||
incoming[heir][id] = E.add(incoming[heir].get(id, 0), E.multiply(E.multiply(released, (E.subtract(1, fraction))), heirs[heir]))
|
||||
for child in outputs.get(id, {}):
|
||||
if world.catalog.entities.has(child) and not CivilizationState.is_space_civilization(child, Global.CivilizationRules):
|
||||
incoming[heir][child] = incoming[heir].get(child, 0.0) + released * fraction * heirs[heir] * outputs[id][child]
|
||||
var remaining: float = held * owner_survival * (1.0 - fraction)
|
||||
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + remaining
|
||||
if (world.catalog.entities.has(child) and (not CivilizationState.is_space_civilization(child, Global.CivilizationRules))):
|
||||
incoming[heir][child] = E.add(incoming[heir].get(child, 0), E.multiply(E.multiply(E.multiply(released, fraction), heirs[heir]), outputs[id][child]))
|
||||
var remaining: Variant = E.multiply(E.multiply(held, owner_survival), (E.subtract(1, fraction)))
|
||||
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), remaining)
|
||||
for successor in outputs.get(id, {}):
|
||||
if not world.catalog.entities.has(successor) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules):
|
||||
if ((not world.catalog.entities.has(successor)) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules)):
|
||||
continue
|
||||
var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor])
|
||||
if not incoming.has(owner_id):
|
||||
var transferred: Variant = E.multiply(E.multiply(E.multiply(held, owner_survival), fraction), E.maximum(0, outputs[id][successor]))
|
||||
if (not incoming.has(owner_id)):
|
||||
incoming[owner_id] = {}
|
||||
incoming[owner_id][successor] = incoming[owner_id].get(successor, 0.0) + transferred
|
||||
incoming[owner_id][successor] = E.add(incoming[owner_id].get(successor, 0), transferred)
|
||||
|
||||
for id in fractions:
|
||||
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id]))
|
||||
target.amount[id] = E.maximum(0, E.multiply(target.amount.get(id, 0), (E.subtract(1, fractions[id]))))
|
||||
|
||||
return incoming
|
||||
|
||||
@@ -279,9 +272,9 @@ func transfer_evolution(target: Dictionary, fractions: Dictionary, outputs: Dict
|
||||
## Commit reservations after newly evolved objects have been credited.
|
||||
func accept_transfers(target: Dictionary, incoming: Dictionary) -> void:
|
||||
for owner_id in incoming:
|
||||
if not target.binds.has(owner_id):
|
||||
if (not target.binds.has(owner_id)):
|
||||
target.binds[owner_id] = {}
|
||||
for id in incoming[owner_id]:
|
||||
var transferred: float = minf(incoming[owner_id][id], target.amount.get(id, 0.0))
|
||||
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + transferred
|
||||
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - transferred)
|
||||
var transferred: Variant = E.minimum(incoming[owner_id][id], target.amount.get(id, 0))
|
||||
target.binds[owner_id][id] = E.add(target.binds[owner_id].get(id, 0), transferred)
|
||||
target.amount[id] = E.maximum(0, E.subtract(target.amount.get(id, 0), transferred))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extends Node
|
||||
class_name Owniverse
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Owns the state and modules. Rules live in the modules below.
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
@@ -16,10 +17,7 @@ var evolution = preload("res://world/owniverse/evolution_coordinator.gd").new(se
|
||||
|
||||
var state: Dictionary = {
|
||||
'version': SimulationTime.STATE_VERSION,
|
||||
'time': {
|
||||
'datetime': 0.0,
|
||||
'year': 0.0,
|
||||
},
|
||||
'time': {'datetime': 0, 'year': 0},
|
||||
'speed': 1,
|
||||
'speed_in_year': 1,
|
||||
'energy': WorldParameters.INITIAL_ENERGY,
|
||||
@@ -43,9 +41,7 @@ var state: Dictionary = {
|
||||
'civilization_stats': {},
|
||||
'civilization_homes': {},
|
||||
'ethics': {},
|
||||
'stats': {
|
||||
'total_amount': {},
|
||||
}
|
||||
'stats': {'total_amount': {}},
|
||||
}
|
||||
|
||||
var speed: int :
|
||||
@@ -53,12 +49,11 @@ var speed: int:
|
||||
return state.speed
|
||||
set(value):
|
||||
state.speed = clampi(value, 1, Global.SPEED_LIMIT)
|
||||
state.speed_in_year = pow(10.0, state.speed - 1)
|
||||
state.speed_in_year = E.power(10, E.subtract(state.speed, 1))
|
||||
|
||||
func evolute(delta: float) -> void:
|
||||
if delta <= 0.0 or not is_finite(delta):
|
||||
func evolute(delta: Variant) -> void:
|
||||
if (E.le(delta, 0) or (not E.finite(delta))):
|
||||
return
|
||||
var datetime: float = state.time.datetime + delta * pow(10.0, speed - 1)
|
||||
if not is_finite(datetime) or not is_finite(datetime * 10.0):
|
||||
return
|
||||
evolution.evolute_to_datetime(datetime)
|
||||
var clock: Dictionary = SimulationTime.advance(state.time, E.approx(delta), speed)
|
||||
if not E.finite(clock.datetime): return
|
||||
evolution.evolute_to_datetime(clock.datetime, clock)
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
extends Node
|
||||
class_name OwniverseEntity
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
var id: String
|
||||
var is_visible: bool = false
|
||||
var unlock: Dictionary = {}
|
||||
var cost: Dictionary = {}
|
||||
var bind: Dictionary = {}
|
||||
var mass: float
|
||||
var mass: Variant
|
||||
var space: int
|
||||
var life_rule: Dictionary = {}
|
||||
var lifespan: float
|
||||
var lifespan_delta: float
|
||||
var auto: float
|
||||
var gamma: float
|
||||
var lifespan: Variant
|
||||
var lifespan_delta: Variant
|
||||
var auto: Variant
|
||||
var gamma: Variant
|
||||
var blast: int
|
||||
var blastability: bool = false
|
||||
var life_product: Dictionary = {}
|
||||
var product_per_year_type: String = ""
|
||||
var product_per_year_amount: float = 0
|
||||
var product_per_year_amount: Variant = 0
|
||||
var voids: int
|
||||
var output_or: Dictionary = {}
|
||||
var output_and: Dictionary = {}
|
||||
@@ -27,49 +28,49 @@ var planets: Dictionary = {}
|
||||
var count_as: String
|
||||
var group_as: String
|
||||
var event_group: String
|
||||
var space_bonus: float
|
||||
var civ_bonus: float
|
||||
var space_bonus: Variant
|
||||
var civ_bonus: Variant
|
||||
var claim: String
|
||||
|
||||
var amount: float = 0.0
|
||||
var amount: Variant = 0
|
||||
|
||||
|
||||
func _init(values: Dictionary) -> void:
|
||||
id = values.id
|
||||
is_visible = values.is_visible == "1"
|
||||
is_visible = E.eq(values.is_visible, "1")
|
||||
unlock = _parse_and_string(values.unlock)
|
||||
cost = _parse_and_string(values.cost)
|
||||
bind = _parse_and_string(values.bind)
|
||||
#print(id, " | ", cost, " | ", bind)
|
||||
mass = float(values.mass)
|
||||
space = int(values.space)
|
||||
mass = E.of(values.mass)
|
||||
space = E.integer(values.space)
|
||||
life_rule = Global.LifeRules.get(id, {}).duplicate(true)
|
||||
if Global.CivilizationRules.has(id):
|
||||
life_rule = {"ethical": true, "span": Global.CivilizationRules[id].span}
|
||||
lifespan = float(life_rule.span) if not life_rule.is_empty() else float(values.lifespan)
|
||||
lifespan_delta = lifespan / Global.batch_count if lifespan > 10 else lifespan
|
||||
auto = float(values.auto)
|
||||
gamma = float(values.gamma)
|
||||
blast = int(values.blast)
|
||||
blastability = str(values.get("blastability", "")) == "1"
|
||||
lifespan = (E.of(life_rule.span) if (not life_rule.is_empty()) else E.of(values.lifespan))
|
||||
lifespan_delta = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
|
||||
auto = E.of(values.auto)
|
||||
gamma = E.of(values.gamma if str(values.gamma).is_valid_float() else "0")
|
||||
blast = E.integer(values.blast)
|
||||
blastability = E.eq(str(values.get("blastability", "")), "1")
|
||||
|
||||
life_product = _parse_and_string(values.life_product)
|
||||
for key in life_product.keys():
|
||||
product_per_year_type = key
|
||||
product_per_year_amount = life_product[key] / lifespan if lifespan != 0 else 0
|
||||
product_per_year_amount = (E.divide(life_product[key], lifespan) if E.ne(lifespan, 0) else 0)
|
||||
#print(id, " | ", lifespan_delta, " | ", lifespan)
|
||||
|
||||
voids = int(values.voids)
|
||||
voids = E.integer(values.voids)
|
||||
output_or = _parse_or_string(values.output)
|
||||
output_and = _parse_and_string(values.output)
|
||||
destroy_level = int(values.destroy_level)
|
||||
destroy_level = E.integer(values.destroy_level)
|
||||
degradation = _parse_and_string(values.degradation)
|
||||
planets = _parse_planet_string(values.planets)
|
||||
count_as = values.count_as
|
||||
group_as = values.group_as
|
||||
event_group = values.event_group
|
||||
space_bonus = float(values.space_bonus)
|
||||
civ_bonus = float(values.civ_bonus)
|
||||
space_bonus = E.of(values.space_bonus)
|
||||
civ_bonus = E.of(values.civ_bonus)
|
||||
claim = values.claim
|
||||
|
||||
|
||||
@@ -78,7 +79,7 @@ func _parse_or_string(source_line: String) -> Dictionary:
|
||||
if source_line.contains("^"):
|
||||
var first_split = source_line.split("^")
|
||||
var second_split = first_split[1].split("%")
|
||||
var probability = float(second_split[1])
|
||||
var probability = E.of(second_split[1])
|
||||
result["chance"] = probability
|
||||
result["positive"] = second_split[0]
|
||||
result["negative"] = first_split[0]
|
||||
@@ -92,8 +93,8 @@ func _parse_and_string(source_line: String) -> Dictionary:
|
||||
|
||||
for elem in first_split:
|
||||
var second_split = elem.split(":")
|
||||
if len(second_split) == 2:
|
||||
result[second_split[0]] = float(second_split[1])
|
||||
if E.eq(len(second_split), 2):
|
||||
result[second_split[0]] = E.of(second_split[1])
|
||||
|
||||
return result
|
||||
|
||||
@@ -101,16 +102,16 @@ func _parse_planet_string(source_line: String) -> Dictionary:
|
||||
var result = {}
|
||||
for elem in source_line.split("&", false):
|
||||
var parts: PackedStringArray = elem.strip_edges().split(":")
|
||||
if parts.size() != 3 or parts[0].strip_edges().is_empty():
|
||||
push_warning("Invalid planet definition for %s: %s" % [id, elem])
|
||||
if (E.ne(parts.size(), 3) or parts[0].strip_edges().is_empty()):
|
||||
push_warning(("Invalid planet definition for %s: %s" % [id, elem]))
|
||||
continue
|
||||
if not parts[1].is_valid_float() or not parts[2].is_valid_float():
|
||||
push_warning("Invalid planet numbers for %s: %s" % [id, elem])
|
||||
if ((not parts[1].is_valid_float()) or (not parts[2].is_valid_float())):
|
||||
push_warning(("Invalid planet numbers for %s: %s" % [id, elem]))
|
||||
continue
|
||||
var count: float = float(parts[1])
|
||||
var chance: float = float(parts[2])
|
||||
if not is_finite(count) or not is_finite(chance) or count < 0.0 or count != floorf(count) or chance < 0.0 or chance > 1.0:
|
||||
push_warning("Planet values out of range for %s: %s" % [id, elem])
|
||||
var count: Variant = E.of(parts[1])
|
||||
var chance: Variant = E.of(parts[2])
|
||||
if ((((((not E.finite(count)) or (not E.finite(chance))) or E.lt(count, 0)) or E.ne(count, E.floor_value(count))) or E.lt(chance, 0)) or E.gt(chance, 1)):
|
||||
push_warning(("Planet values out of range for %s: %s" % [id, elem]))
|
||||
continue
|
||||
# Planet chances are fractions (0.01 = 1%), unlike output_or percentages.
|
||||
result[parts[0].strip_edges()] = {"amount": count, "chance": chance}
|
||||
|
||||
@@ -1,60 +1,51 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## 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):
|
||||
static func accumulate_whole(target: Dictionary, field: String, key: String, value: Variant) -> Variant:
|
||||
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)
|
||||
var expected: Variant = E.add(E.maximum(0, value), target[field].get(key, 0))
|
||||
var whole: Variant = E.floor_value(expected)
|
||||
target[field][key] = E.maximum(0, E.subtract(expected, whole))
|
||||
return whole
|
||||
|
||||
|
||||
static func allocate_whole(requests: Dictionary, limit: float) -> Dictionary:
|
||||
var total: float = 0.0
|
||||
static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||
var total: Variant = 0
|
||||
for amount in requests.values():
|
||||
total += floorf(maxf(0.0, amount))
|
||||
var available: float = floorf(minf(maxf(0.0, limit), total))
|
||||
total = E.add(total, E.floor_value(E.maximum(0, amount)))
|
||||
var available: Variant = E.floor_value(E.minimum(E.maximum(0, limit), total))
|
||||
var result: Dictionary = {}
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 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))
|
||||
var expected: Variant = (E.divide(E.multiply(E.floor_value(E.maximum(0, requests[id])), available), total) if E.gt(total, 0) else 0)
|
||||
result[id] = E.floor_value(expected)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(expected, result[id])})
|
||||
order.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for item : Dictionary in order:
|
||||
if assigned >= available:
|
||||
if E.ge(assigned, available):
|
||||
break
|
||||
if result[item.id] < floorf(requests[item.id]):
|
||||
result[item.id] += 1.0
|
||||
assigned += 1.0
|
||||
if E.lt(result[item.id], E.floor_value(requests[item.id])):
|
||||
result[item.id] = E.add(result[item.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
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
|
||||
static func production_step(costs: Array):
|
||||
var step = 1
|
||||
for cost in costs:
|
||||
if E.le(cost, 0): continue
|
||||
var denominator = E.make(E.parts(cost)[1])
|
||||
step = E.multiply(E.divide(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)
|
||||
static func whole_gcd(a: Variant, b: Variant) -> Variant:
|
||||
while E.gt(b, 0):
|
||||
var remainder: Variant = E.remainder(a, b)
|
||||
a = b
|
||||
b = remainder
|
||||
return a
|
||||
@@ -62,50 +53,50 @@ static func whole_gcd(a: float, b: float) -> float:
|
||||
|
||||
static func normalize_distribution(values: Dictionary) -> Dictionary:
|
||||
var normalized_distribution: Dictionary = {}
|
||||
var distribution_total: float = 0
|
||||
var distribution_total: Variant = 0
|
||||
for id in values:
|
||||
if values[id] > 0:
|
||||
distribution_total += values[id]
|
||||
if E.gt(values[id], 0):
|
||||
distribution_total = E.add(distribution_total, values[id])
|
||||
normalized_distribution[id] = values[id]
|
||||
for id in normalized_distribution:
|
||||
normalized_distribution[id] /= distribution_total
|
||||
normalized_distribution[id] = E.divide(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
|
||||
static func sum(pop: Dictionary) -> Variant:
|
||||
var result: Variant = 0
|
||||
for n in pop.values(): result = E.add(result, n)
|
||||
return result
|
||||
|
||||
|
||||
static func split(remainders: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
||||
static func split(remainders: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||
var result: Dictionary = {}
|
||||
var sum_weights: float = sum(weights)
|
||||
if count <= 0.0 or sum_weights <= 0.0: return result
|
||||
var sum_weights: Variant = sum(weights)
|
||||
if (E.le(count, 0) or E.le(sum_weights, 0)): return result
|
||||
var expected: Dictionary = {}
|
||||
var positive_sum: float = 0.0
|
||||
var positive_sum: Variant = 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])
|
||||
expected[id] = E.add(E.divide(E.multiply(count, weights[id]), sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
|
||||
var order: Array = []
|
||||
var assigned: float = 0.0
|
||||
var assigned: Variant = 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))
|
||||
var normalized: Variant = (E.divide(E.multiply(E.maximum(0, expected[id]), count), positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||
result[id] = E.floor_value(normalized)
|
||||
assigned = E.add(assigned, result[id])
|
||||
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
|
||||
order.sort_custom(func ( a , b ) : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for entry in order:
|
||||
if assigned >= count: break
|
||||
result[entry.id] += 1.0
|
||||
assigned += 1.0
|
||||
if E.ge(assigned, count): break
|
||||
result[entry.id] = E.add(result[entry.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
for id in weights:
|
||||
remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
||||
remainders[E.add(E.add(key, ":"), id)] = E.clamp_value(E.subtract(expected[id], result[id]), E.neg(1), 1)
|
||||
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)
|
||||
static func accumulate_rate(remainders: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
|
||||
var expected: Variant = E.add(E.multiply(count, E.maximum(0, probability)), remainders.get(key, 0))
|
||||
var actual: Variant = E.floor_value(expected)
|
||||
remainders[key] = E.maximum(0, E.subtract(expected, actual))
|
||||
return actual
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
extends RefCounted
|
||||
## datetime is accumulated time; year is the last processed tenth of a year.
|
||||
const STATE_VERSION: String = "0.1.1"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const STATE_VERSION: String = "0.2.0"
|
||||
const TICKS_PER_YEAR: int = 1000000000
|
||||
|
||||
static func processed_year(datetime: float) -> float:
|
||||
# Remove only sub-nanotick accumulation noise at decimal boundaries.
|
||||
# A relative epsilon would incorrectly advance large ages by whole years.
|
||||
return floorf(snappedf(datetime * 10.0, 1e-9)) / 10.0
|
||||
static func processed_year(datetime):
|
||||
return E.divide(E.floor_value(E.multiply(datetime, 10)), 10)
|
||||
|
||||
static func elapsed_years(from_year: float, to_year: float) -> float:
|
||||
# Subtract integer tenths before returning to float years. Otherwise 0.3-0.2
|
||||
# can fall below 0.1 and postpone a whole unit of resource production.
|
||||
return (roundf(to_year * 10.0) - roundf(from_year * 10.0)) / 10.0
|
||||
static func elapsed_years(from_year, to_year):
|
||||
return E.subtract(to_year, from_year)
|
||||
|
||||
## Frame fractions stay small; the universe age is an exact integer tick count.
|
||||
static func advance(time: Dictionary, delta: float, speed: int) -> Dictionary:
|
||||
var ticks = E.round_value(E.multiply(time.datetime, TICKS_PER_YEAR))
|
||||
var increment: float = delta * pow(10.0, speed - 1) * TICKS_PER_YEAR + float(time.get("tick_remainder", 0.0))
|
||||
var whole: float = roundf(increment) if absf(increment - roundf(increment)) < 1e-6 else floorf(increment)
|
||||
ticks = E.add(ticks, E.of(whole))
|
||||
var datetime = E.divide(ticks, TICKS_PER_YEAR)
|
||||
return {"datetime": datetime, "year": processed_year(datetime), "ticks": ticks, "tick_remainder": increment - whole}
|
||||
|
||||
@@ -1,118 +1,117 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||
|
||||
## Testable optimization switch; both paths preserve the same event order.
|
||||
var direct_completions: bool = true
|
||||
|
||||
func add_output_terms(outputs: Dictionary, terms: String, count: float) -> void:
|
||||
if count <= 0.0:
|
||||
func add_output_terms(outputs: Dictionary, terms: String, count: Variant) -> void:
|
||||
if E.le(count, 0):
|
||||
return
|
||||
for term in terms.split("&"):
|
||||
var parts: PackedStringArray = term.strip_edges().split(":")
|
||||
var id: String = parts[0].strip_edges()
|
||||
var amount: float = float(parts[1]) if parts.size() == 2 else 1.0
|
||||
if id != "" and amount > 0.0:
|
||||
outputs[id] = outputs.get(id, 0.0) + count * amount
|
||||
var amount: Variant = (E.of(parts[1]) if E.eq(parts.size(), 2) else 1)
|
||||
if (E.ne(id, "") and E.gt(amount, 0)):
|
||||
outputs[id] = E.add(outputs.get(id, 0), E.multiply(count, amount))
|
||||
|
||||
|
||||
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||
if not entity.life_rule.is_empty():
|
||||
func evolution_products(target: Dictionary, entity: OwniverseEntity, count: Variant) -> Dictionary:
|
||||
if (not entity.life_rule.is_empty()):
|
||||
return world.life.life_products(target, entity, count)
|
||||
var outputs: Dictionary = {}
|
||||
for id in entity.output_and:
|
||||
outputs[id] = count * maxf(0.0, entity.output_and[id])
|
||||
if not entity.output_or.is_empty():
|
||||
outputs[id] = E.multiply(count, E.maximum(0, entity.output_and[id]))
|
||||
if (not entity.output_or.is_empty()):
|
||||
# The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent.
|
||||
var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0 * WorldParameters.effective(target, "Exp"), 0.0, 1.0)
|
||||
if not target.has("evolution_remainders"):
|
||||
var chance: Variant = E.clamp_value(E.multiply(E.divide(entity.output_or.get("chance", 0), 100), WorldParameters.effective(target, "Exp")), 0, 1)
|
||||
if (not target.has("evolution_remainders")):
|
||||
target.evolution_remainders = {}
|
||||
var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0)
|
||||
var positive: float = minf(count, floorf(snappedf(expected, 1.0e-12)))
|
||||
target.evolution_remainders[entity.id] = maxf(0.0, expected - positive)
|
||||
var expected: Variant = E.add(E.multiply(count, chance), target.evolution_remainders.get(entity.id, 0))
|
||||
var positive: Variant = E.minimum(count, E.floor_value(expected))
|
||||
target.evolution_remainders[entity.id] = E.maximum(0, E.subtract(expected, positive))
|
||||
add_output_terms(outputs, str(entity.output_or.get("positive", "")), positive)
|
||||
add_output_terms(outputs, str(entity.output_or.get("negative", "")), count - positive)
|
||||
add_output_terms(outputs, str(entity.output_or.get("negative", "")), E.subtract(count, positive))
|
||||
# Entity.blast is authoritative even if imported output still contains
|
||||
# blast:N. Zero disables the event; never add both representations.
|
||||
outputs.erase("blast")
|
||||
if entity.blast > 0:
|
||||
outputs["blast"] = count * entity.blast * WorldParameters.effective(target, "Exp")
|
||||
if E.gt(entity.blast, 0):
|
||||
outputs["blast"] = E.multiply(E.multiply(count, entity.blast), WorldParameters.effective(target, "Exp"))
|
||||
return outputs
|
||||
|
||||
|
||||
func create_planets(target: Dictionary, star_id: String, count: float,
|
||||
from_year: float, to_year: float, metallicity: float) -> void:
|
||||
if count <= 0.0 or world.catalog.entities[star_id].planets.is_empty():
|
||||
func create_planets(target: Dictionary, star_id: String, count: Variant, from_year: Variant, to_year: Variant, metallicity: Variant) -> void:
|
||||
if (E.le(count, 0) or world.catalog.entities[star_id].planets.is_empty()):
|
||||
return
|
||||
if not target.binds.has(star_id):
|
||||
if (not target.binds.has(star_id)):
|
||||
target.binds[star_id] = {}
|
||||
for id in world.catalog.entities[star_id].planets:
|
||||
if not world.catalog.entities.has(id):
|
||||
if (not world.catalog.entities.has(id)):
|
||||
continue
|
||||
var rule: Dictionary = world.catalog.entities[star_id].planets[id]
|
||||
var amount: float = QuantityMath.accumulate_whole(target, "planet_remainders", star_id + ":" + id,
|
||||
count * rule.amount * rule.chance * maxf(0.0, metallicity))
|
||||
var amount: Variant = QuantityMath.accumulate_whole(target, "planet_remainders", E.add(E.add(star_id, ":"), id), E.multiply(E.multiply(E.multiply(count, rule.amount), rule.chance), E.maximum(0, metallicity)))
|
||||
amount = world.ledger.credit(target, id, amount)
|
||||
target.amount[id] -= amount
|
||||
target.binds[star_id][id] = target.binds[star_id].get(id, 0.0) + amount
|
||||
target.amount[id] = E.subtract(target.amount[id], amount)
|
||||
target.binds[star_id][id] = E.add(target.binds[star_id].get(id, 0), amount)
|
||||
world.batches.append_batch(target, id, amount, from_year, to_year)
|
||||
|
||||
|
||||
func stellar_successors(products: Dictionary) -> Dictionary:
|
||||
var weights: Dictionary = {}
|
||||
for id in products:
|
||||
if world.catalog.entities.has(id) and (world.catalog.entities[id].group_as in ["Star", "BH"] or world.catalog.entities[id].event_group == "SNGRBB" or world.catalog.entities[id].count_as == "BD"):
|
||||
if (world.catalog.entities.has(id) and (((E.contains(["Star", "BH"], world.catalog.entities[id].group_as) or E.eq(world.catalog.entities[id].event_group, "SNGRBB")) or E.eq(world.catalog.entities[id].count_as, "BD")))):
|
||||
weights[id] = products[id]
|
||||
return QuantityMath.normalize_distribution(weights)
|
||||
|
||||
|
||||
func destroy_planets(target: Dictionary, owner_id: String, ratio: float, level: int, cause: String = "local") -> void:
|
||||
if ratio <= 0.0 or level not in [1, 2]:
|
||||
func destroy_planets(target: Dictionary, owner_id: String, ratio: Variant, level: int, cause: String = "local") -> void:
|
||||
if (E.le(ratio, 0) or not E.contains([1, 2], level)):
|
||||
return
|
||||
var held: Dictionary = target.binds.get(owner_id, {})
|
||||
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||
var rocky: float = 0.0
|
||||
var rocky: Variant = 0
|
||||
for id in held.keys():
|
||||
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Planet":
|
||||
if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Planet")):
|
||||
continue
|
||||
if level == 1 and not world.catalog.entities[id].blastability:
|
||||
if (E.eq(level, 1) and (not world.catalog.entities[id].blastability)):
|
||||
continue
|
||||
var key: String = cause + ":" + str(level) + ":" + owner_id + ":" + id
|
||||
var destroyed: float = minf(held[id], QuantityMath.accumulate_whole(target, "destruction_remainders", key, held[id] * minf(1.0, ratio)))
|
||||
if destroyed == held[id]:
|
||||
target.destruction_remainders[key] = 0.0
|
||||
if destroyed <= 0.0:
|
||||
var key: String = E.add(E.add(E.add(E.add(E.add(E.add(cause, ":"), str(level)), ":"), owner_id), ":"), id)
|
||||
var destroyed: Variant = E.minimum(held[id], QuantityMath.accumulate_whole(target, "destruction_remainders", key, E.multiply(held[id], E.minimum(1, ratio))))
|
||||
if E.eq(destroyed, held[id]):
|
||||
target.destruction_remainders[key] = 0
|
||||
if E.le(destroyed, 0):
|
||||
continue
|
||||
held[id] -= destroyed
|
||||
world.batches.scale_batches(target, id, 1.0 - destroyed / physical[id])
|
||||
if level == 1:
|
||||
rocky += destroyed
|
||||
if rocky > 0.0:
|
||||
held[id] = E.subtract(held[id], destroyed)
|
||||
world.batches.scale_batches(target, id, E.subtract(1, E.divide(destroyed, physical[id])))
|
||||
if E.eq(level, 1):
|
||||
rocky = E.add(rocky, destroyed)
|
||||
if E.gt(rocky, 0):
|
||||
rocky = world.ledger.credit(target, "RP", rocky)
|
||||
target.amount.RP -= rocky
|
||||
held.RP = held.get("RP", 0.0) + rocky
|
||||
target.amount.RP = E.subtract(target.amount.RP, rocky)
|
||||
held.RP = E.add(held.get("RP", 0), rocky)
|
||||
|
||||
|
||||
func apply_blast(target: Dictionary, power: float, environment: Dictionary) -> void:
|
||||
var hits: float = QuantityMath.accumulate_whole(target, "blast_remainders", "hits", power * environment.blast)
|
||||
func apply_blast(target: Dictionary, power: Variant, environment: Dictionary) -> void:
|
||||
var hits: Variant = QuantityMath.accumulate_whole(target, "blast_remainders", "hits", E.multiply(power, environment.blast))
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||
var weights: Dictionary = {}
|
||||
for id in world.catalog.entities:
|
||||
if not world.catalog.entities[id].planets.is_empty() and quantities.get(id, 0.0) > 0.0:
|
||||
if ((not world.catalog.entities[id].planets.is_empty()) and E.gt(quantities.get(id, 0), 0)):
|
||||
weights[id] = quantities[id]
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(weights)
|
||||
# Fractional exposure is deterministic; destruction itself remains whole.
|
||||
for id in distribution:
|
||||
destroy_planets(target, id, hits * distribution[id] / quantities[id] * environment.density, 1, "blast")
|
||||
destroy_planets(target, id, E.multiply(E.divide(E.multiply(hits, distribution[id]), quantities[id]), environment.density), 1, "blast")
|
||||
|
||||
|
||||
func uses_short_lived_fast_path(id: String) -> bool:
|
||||
if not direct_completions: return false
|
||||
if (not direct_completions): return false
|
||||
# Only terminal stellar events with linear outputs and no living successors.
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if entity.event_group != "SNGRBB" or not entity.output_or.is_empty() or not entity.life_rule.is_empty():
|
||||
if ((E.ne(entity.event_group, "SNGRBB") or (not entity.output_or.is_empty())) or (not entity.life_rule.is_empty())):
|
||||
return false
|
||||
for successor in entity.output_and:
|
||||
if world.catalog.entities.has(successor) and world.catalog.entities[successor].lifespan > 0.0:
|
||||
if (world.catalog.entities.has(successor) and E.gt(world.catalog.entities[successor].lifespan, 0)):
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
extends "res://world/owniverse/simulation_module.gd"
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
|
||||
func stellar_environment(source: Dictionary) -> Dictionary:
|
||||
var quantities: Dictionary = world.ledger.physical_amounts(source)
|
||||
# SMBH is a self-group in GI; its quantity stores mass, not object count.
|
||||
var mass: float = source.amount.get("SMBH", 0.0)
|
||||
var volume: float = quantities.get("S", 0.0)
|
||||
var mass: Variant = source.amount.get("SMBH", 0)
|
||||
var volume: Variant = quantities.get("S", 0)
|
||||
for id in quantities:
|
||||
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||
if id in ["H", "He", "Met", "SMBH"]:
|
||||
mass += quantities[id]
|
||||
if E.contains(["H", "He", "Met", "SMBH"], id):
|
||||
mass = E.add(mass, quantities[id])
|
||||
else:
|
||||
mass += quantities[id] * maxf(0.0, entity.mass)
|
||||
volume += quantities[id] * maxf(0.0, entity.space)
|
||||
volume += quantities[id] * maxf(0.0, entity.voids) * 1.0e22
|
||||
var metallicity: float = 0.0
|
||||
if mass > 0.0:
|
||||
metallicity = maxf(0.0, 0.4 * log(100.0 * source.amount.get("Met", 0.0) / mass + 0.11) + 0.88)
|
||||
var base_mass: float = 65536.0 * (pow(mass, 0.1) if mass > 0.0 and mass < 1.0e10 else (10.0 if mass >= 1.0e10 else 1.0))
|
||||
return {
|
||||
"metallicity": metallicity,
|
||||
"density": 1.23456e-7 * sqrt(base_mass + mass) / pow(maxf(1.0, volume), 1.0 / 6.0),
|
||||
"blast": clampf(source.amount.get("S", 0.0) / volume, 0.0, 1.0) if volume > 0.0 else 1.0,
|
||||
}
|
||||
mass = E.add(mass, E.multiply(quantities[id], E.maximum(0, entity.mass)))
|
||||
volume = E.add(volume, E.multiply(quantities[id], E.maximum(0, entity.space)))
|
||||
volume = E.add(volume, E.multiply(E.multiply(quantities[id], E.maximum(0, entity.voids)), E.of("1.0e22")))
|
||||
var metallicity: Variant = 0
|
||||
if E.gt(mass, 0):
|
||||
metallicity = E.maximum(0, E.add(E.multiply(E.of("0.4"), E.logarithm(E.add(E.divide(E.multiply(100, source.amount.get("Met", 0)), mass), E.of("0.11")))), E.of("0.88")))
|
||||
var base_mass: Variant = E.multiply(65536, ((E.power(mass, E.of("0.1")) if (E.gt(mass, 0) and E.lt(mass, E.of("1.0e10"))) else ((10 if E.ge(mass, E.of("1.0e10")) else 1)))))
|
||||
return {"metallicity": metallicity, "density": E.divide(E.multiply(E.of("1.23456e-7"), E.square_root(E.add(base_mass, mass))), E.power(E.maximum(1, volume), E.divide(1, 6))), "blast": (E.clamp_value(E.divide(source.amount.get("S", 0), volume), 0, 1) if E.gt(volume, 0) else 1)}
|
||||
|
||||
|
||||
func modify_world_parameter(key: String, change: float) -> void:
|
||||
if key not in ["Bur", "Exp", "Agg"] or not is_finite(change): return
|
||||
var old_multiplier: float = WorldParameters.effective(world.state, key)
|
||||
if not world.state.has("world_parameters"): world.state.world_parameters = WorldParameters.defaults()
|
||||
world.state.world_parameters[key] = clampf(WorldParameters.value(world.state, key) + change, 1.0, 19.0)
|
||||
if key == "Bur":
|
||||
var ratio: float = old_multiplier / WorldParameters.effective(world.state, key)
|
||||
var now: float = world.state.time.year
|
||||
func modify_world_parameter(key: String, change: Variant) -> void:
|
||||
if (not E.contains(["Bur", "Exp", "Agg"], key) or (not E.finite(change))): return
|
||||
var old_multiplier: Variant = WorldParameters.effective(world.state, key)
|
||||
if (not world.state.has("world_parameters")): world.state.world_parameters = WorldParameters.defaults()
|
||||
world.state.world_parameters[key] = E.clamp_value(E.add(WorldParameters.value(world.state, key), change), 1, 19)
|
||||
if E.eq(key, "Bur"):
|
||||
var ratio: Variant = E.divide(old_multiplier, WorldParameters.effective(world.state, key))
|
||||
var now: Variant = world.state.time.year
|
||||
for id in world.state.batches:
|
||||
if not world.catalog.entities.has(id) or world.catalog.entities[id].group_as != "Star": continue
|
||||
if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Star")): continue
|
||||
for batch : Dictionary in world.state.batches[id]:
|
||||
# Only future survival time changes. Birth dates and the fractional
|
||||
# withdrawal carry remain intact; already elapsed time is not replayed.
|
||||
for field in ["start_year", "end_year", "processed_until"]:
|
||||
if batch.has(field): batch[field] = now + maxf(0.0, batch[field] - now) * ratio
|
||||
if batch.has(field): batch[field] = E.add(now, E.multiply(E.maximum(0, E.subtract(batch[field], now)), ratio))
|
||||
|
||||
@@ -1,112 +1,106 @@
|
||||
extends RefCounted
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const INITIAL_ENERGY = 1.0e27
|
||||
static var INITIAL_ENERGY = E.of("1.0e27")
|
||||
const TRENDS = ["Antitheism", "Integration", "Xenocide", "Reconstruction", "Spirituality"]
|
||||
const WEIGHTS = {
|
||||
"m": [0, 2, 2, 0, 6], "l": [6, 1, 1, 2, 0],
|
||||
"w": [2, 0, 6, 0, 2], "d": [0, 6, 0, 1, 3],
|
||||
"c": [1, 3, 3, 3, 0], "b": [3, 0, 0, 6, 1],
|
||||
}
|
||||
const WEIGHTS = {"m": [0, 2, 2, 0, 6], "l": [6, 1, 1, 2, 0], "w": [2, 0, 6, 0, 2], "d": [0, 6, 0, 1, 3], "c": [1, 3, 3, 3, 0], "b": [3, 0, 0, 6, 1]}
|
||||
|
||||
static func defaults() -> Dictionary:
|
||||
return {"Bur": 10.0, "Exp": 10.0, "Agg": 10.0,
|
||||
"Federation": 1.0, "Enlightenment": 1.0, "Order": 1.0}
|
||||
return {"Bur": 10, "Exp": 10, "Agg": 10, "Federation": 1, "Enlightenment": 1, "Order": 1}
|
||||
|
||||
static func trend_defaults() -> Dictionary:
|
||||
return {"Antitheism": 0.0, "Integration": 0.0, "Xenocide": 0.0,
|
||||
"Reconstruction": 0.0, "Spirituality": 0.0}
|
||||
return {"Antitheism": 0, "Integration": 0, "Xenocide": 0, "Reconstruction": 0, "Spirituality": 0}
|
||||
|
||||
static func value(source: Dictionary, key: String) -> float:
|
||||
return source.get("world_parameters", {}).get(key, defaults().get(key, 1.0))
|
||||
static func value(source: Dictionary, key: String) -> Variant:
|
||||
return source.get("world_parameters", {}).get(key, defaults().get(key, 1))
|
||||
|
||||
static func effective(source: Dictionary, key: String) -> float:
|
||||
return pow(10.0, (clampf(value(source, key), 1.0, 19.0) - 10.0) / 9.0)
|
||||
static func effective(source: Dictionary, key: String) -> Variant:
|
||||
return E.power(10, E.divide((E.subtract(E.clamp_value(value(source, key), 1, 19), 10)), 9))
|
||||
|
||||
static func life_modifiers(source: Dictionary) -> Dictionary:
|
||||
var aggression: float = effective(source, "Agg")
|
||||
return {"progress": aggression,
|
||||
"degradation": (1.0 - (1.0 - aggression) / 20.0) / value(source, "Order"),
|
||||
"war": 1.0 - (1.0 - aggression) / 5.0,
|
||||
"colonization": 1.0 - (1.0 - aggression) / 2.0}
|
||||
var aggression: Variant = effective(source, "Agg")
|
||||
return {"progress": aggression, "degradation": E.divide((E.subtract(1, E.divide((E.subtract(1, aggression)), 20))), value(source, "Order")), "war": E.subtract(1, E.divide((E.subtract(1, aggression)), 5)), "colonization": E.subtract(1, E.divide((E.subtract(1, aggression)), 2))}
|
||||
|
||||
static func production(source: Dictionary, entity) -> float:
|
||||
if entity.event_group == "SNGRBB":
|
||||
static func production(source: Dictionary, entity) -> Variant:
|
||||
if E.eq(entity.event_group, "SNGRBB"):
|
||||
return effective(source, "Exp")
|
||||
if entity.group_as == "Star":
|
||||
var burn: float = effective(source, "Bur")
|
||||
return sqrt(burn) if burn > 1.0 else burn
|
||||
return 1.0
|
||||
if E.eq(entity.group_as, "Star"):
|
||||
var burn: Variant = effective(source, "Bur")
|
||||
return (E.square_root(burn) if E.gt(burn, 1) else burn)
|
||||
return 1
|
||||
|
||||
static func density_display(density: float) -> Dictionary:
|
||||
if density <= 1.0e-5:
|
||||
static func density_display(density: Variant) -> Dictionary:
|
||||
if E.le(density, E.of("1.0e-5")):
|
||||
# Unity had log10(density) - 7 here; +7 maps 1e-7..1e-5 to 0..1.
|
||||
return {"mode": "DensityRip", "position": clampf((log(maxf(density, 1e-7)) / log(10.0) + 7.0) / 2.0, 0.0, 1.0)}
|
||||
if density >= 0.9:
|
||||
return {"mode": "DensityCrunch", "position": clampf((density - 0.9) * 10.0, 0.0, 1.0)}
|
||||
return {"mode": "Density", "position": clampf(log(density * 1.0e5) / log(9.0e4), 0.0, 1.0)}
|
||||
return {"mode": "DensityRip", "position": E.clamp_value(E.divide((E.add(E.divide(E.logarithm(E.maximum(density, E.of("1e-7"))), E.logarithm(10)), 7)), 2), 0, 1)}
|
||||
if E.ge(density, E.of("0.9")):
|
||||
return {"mode": "DensityCrunch", "position": E.clamp_value(E.multiply((E.subtract(density, E.of("0.9"))), 10), 0, 1)}
|
||||
return {"mode": "Density", "position": E.clamp_value(E.divide(E.logarithm(E.multiply(density, 100000)), E.logarithm(90000)), 0, 1)}
|
||||
|
||||
## Unity ApocalypseCheck thresholds and WarningUI colors; visual warning only.
|
||||
static func density_warning_color(density: float) -> Color:
|
||||
if density >= 1.0: return Color.html("00c0ffff")
|
||||
if density >= 0.97: return Color.html("20b0dfbf")
|
||||
if density >= 0.9: return Color.html("40a0c07f")
|
||||
if density <= 1.0e-7: return Color.html("ff0000ff")
|
||||
if density <= 1.0e-6: return Color.html("df2020bf")
|
||||
if density <= 1.0e-5: return Color.html("c040407f")
|
||||
return Color(0.0, 0.0, 0.0, 0.0)
|
||||
static func density_warning_color(density: Variant) -> Color:
|
||||
if E.ge(density, 1): return Color.html("00c0ffff")
|
||||
if E.ge(density, E.of("0.97")): return Color.html("20b0dfbf")
|
||||
if E.ge(density, E.of("0.9")): return Color.html("40a0c07f")
|
||||
if E.le(density, E.of("1.0e-7")): return Color.html("ff0000ff")
|
||||
if E.le(density, E.of("1.0e-6")): return Color.html("df2020bf")
|
||||
if E.le(density, E.of("1.0e-5")): return Color.html("c040407f")
|
||||
return Color(0, 0, 0, 0)
|
||||
|
||||
## Integrate the population present at the start of the simulation interval.
|
||||
## Thresholds use whole event counts; no per-year loop at high speeds.
|
||||
static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, years: float) -> void:
|
||||
if years <= 0.0: return
|
||||
if not target.has("world_parameters"): target.world_parameters = defaults()
|
||||
if not target.has("trends"): target.trends = trend_defaults()
|
||||
var rates: Array = [0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
static func advance(previous: Dictionary, target: Dictionary, rules: Dictionary, years: Variant) -> void:
|
||||
if E.le(years, 0): return
|
||||
if (not target.has("world_parameters")): target.world_parameters = defaults()
|
||||
if (not target.has("trends")): target.trends = trend_defaults()
|
||||
var rates: Array = [0, 0, 0, 0, 0]
|
||||
var stages: Array = []
|
||||
for id in rules:
|
||||
if str(rules[id].home) != "": stages.append(id)
|
||||
if E.ne(str(rules[id].home), ""): stages.append(id)
|
||||
stages.sort()
|
||||
var weight: float = 1.0
|
||||
var weight: Variant = 1
|
||||
for id in stages:
|
||||
var population: Dictionary = previous.get("ethics", {}).get(id, {})
|
||||
var count: float = 0.0
|
||||
for n in population.values(): count += n
|
||||
if count > 0.0:
|
||||
var count: Variant = 0
|
||||
for n in population.values(): count = E.add(count, n)
|
||||
if E.gt(count, 0):
|
||||
for ethic in population:
|
||||
for i in TRENDS.size():
|
||||
rates[i] += population[ethic] / count * WEIGHTS[ethic][i] * weight
|
||||
weight *= 2.0
|
||||
rates[i] = E.add(rates[i], E.multiply(E.multiply(E.divide(population[ethic], count), WEIGHTS[ethic][i]), weight))
|
||||
weight = E.multiply(weight, 2)
|
||||
for i in TRENDS.size():
|
||||
var name: String = TRENDS[i]
|
||||
var accumulated: float = target.trends[name] + rates[i] * years * 1.0e-9
|
||||
var events: float = floorf(snappedf(accumulated, 1.0e-12))
|
||||
target.trends[name] = maxf(0.0, accumulated - events)
|
||||
if events <= 0.0: continue
|
||||
var key: String = "trend:" + name
|
||||
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + events
|
||||
if name == "Integration": _integration(target, events)
|
||||
var accumulated: Variant = E.add(target.trends[name], E.multiply(E.multiply(rates[i], years), E.of("1.0e-9")))
|
||||
var events: Variant = E.floor_value(accumulated)
|
||||
target.trends[name] = E.maximum(0, E.subtract(accumulated, events))
|
||||
if E.le(events, 0): continue
|
||||
var key: String = E.add("trend:", name)
|
||||
target.civilization_stats[key] = E.add(target.civilization_stats.get(key, 0), events)
|
||||
if E.eq(name, "Integration"): _integration(target, events)
|
||||
|
||||
static func _integration(target: Dictionary, events: float) -> void:
|
||||
static func _integration(target: Dictionary, events: Variant) -> void:
|
||||
# Equal-probability outcomes in Unity. Use a saved RNG and bounded aggregate
|
||||
# sampling for large steps, so thousands of triggers do not stall a frame.
|
||||
var rng := RandomNumberGenerator.new()
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.seed = 76123
|
||||
if target.civilization_stats.has("trend_rng"):
|
||||
rng.state = int(target.civilization_stats.trend_rng)
|
||||
var counts: Array = [0.0, 0.0, 0.0]
|
||||
if events <= 256.0:
|
||||
for i in int(events): counts[rng.randi_range(0, 2)] += 1.0
|
||||
rng.state = E.integer(target.civilization_stats.trend_rng)
|
||||
var counts: Array = [0, 0, 0]
|
||||
if E.le(events, 256):
|
||||
for i in E.integer(events):
|
||||
var chosen: int = rng.randi_range(0, 2)
|
||||
counts[chosen] = E.add(counts[chosen], 1)
|
||||
else:
|
||||
counts[0] = clampf(roundf(rng.randfn(events / 3.0, sqrt(events * 2.0 / 9.0))), 0.0, events)
|
||||
var rest: float = events - counts[0]
|
||||
counts[1] = clampf(roundf(rng.randfn(rest / 2.0, sqrt(rest / 4.0))), 0.0, rest)
|
||||
counts[2] = rest - counts[1]
|
||||
counts[0] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(events, 3)), E.approx(E.square_root(E.divide(E.multiply(events, 2), 9)))))), 0, events)
|
||||
var rest: Variant = E.subtract(events, counts[0])
|
||||
counts[1] = E.clamp_value(E.round_value(E.of(rng.randfn(E.approx(E.divide(rest, 2)), E.approx(E.square_root(E.divide(rest, 4)))))), 0, rest)
|
||||
counts[2] = E.subtract(rest, counts[1])
|
||||
target.civilization_stats.trend_rng = rng.state
|
||||
var parameters: Dictionary = target.world_parameters
|
||||
parameters.Agg = maxf(1.0, parameters.Agg - counts[0] - counts[1])
|
||||
parameters.Federation += counts[0] * 0.1
|
||||
parameters.Enlightenment += counts[1] * 0.2
|
||||
parameters.Order += counts[2] * 0.1
|
||||
parameters.Agg = E.maximum(1, E.subtract(E.subtract(parameters.Agg, counts[0]), counts[1]))
|
||||
parameters.Federation = E.add(parameters.Federation, E.multiply(counts[0], E.of("0.1")))
|
||||
parameters.Enlightenment = E.add(parameters.Enlightenment, E.multiply(counts[1], E.of("0.2")))
|
||||
parameters.Order = E.add(parameters.Order, E.multiply(counts[2], E.of("0.1")))
|
||||
for i in 3:
|
||||
var key: String = ["Federation", "Enlightenment", "Order"][i]
|
||||
target.civilization_stats[key] = target.civilization_stats.get(key, 0.0) + counts[i]
|
||||
target.civilization_stats[key] = E.add(target.civilization_stats.get(key, 0), counts[i])
|
||||
|
||||
+13
-8
@@ -99,6 +99,7 @@ size_flags_vertical = 3
|
||||
script = ExtResource("3_k83x5")
|
||||
|
||||
[node name="World" type="VBoxContainer" parent="Stage" unique_id=1878643860]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
@@ -1058,10 +1059,9 @@ texture = ExtResource("61_d36xh")
|
||||
stretch_mode = 3
|
||||
|
||||
[node name="Credits" type="VBoxContainer" parent="Stage" unique_id=785042592]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
offset_right = 1080.0
|
||||
offset_bottom = 1920.0
|
||||
script = ExtResource("62_o2i0n")
|
||||
|
||||
[node name="TopMenu" type="HBoxContainer" parent="Stage/Credits" unique_id=1983527467]
|
||||
@@ -1088,15 +1088,20 @@ texture = ExtResource("62_vu83j")
|
||||
custom_minimum_size = Vector2(1080, 0)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Highlight" type="TextureRect" parent="Stage/Credits/Logo" unique_id=1035898922]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
texture = ExtResource("64_rur7c")
|
||||
|
||||
[node name="Lamp" type="TextureButton" parent="Stage/Credits/Logo" unique_id=1059180893]
|
||||
layout_mode = 2
|
||||
texture_normal = ExtResource("65_y1rwn")
|
||||
|
||||
[node name="Highlight" type="TextureRect" parent="Stage/Credits/Logo" unique_id=1035898922]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
texture = ExtResource("64_rur7c")
|
||||
|
||||
[node name="Control" type="Control" parent="Stage/Credits" unique_id=1835435348]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="ItemAmountUpdate" type="Timer" parent="Stage" unique_id=1953928024]
|
||||
wait_time = 0.2
|
||||
autostart = true
|
||||
|
||||
Reference in New Issue
Block a user