Compare commits
8 Commits
c2313e9163
...
testing
| Author | SHA1 | Date | |
|---|---|---|---|
| e94093ac17 | |||
| c4e7302567 | |||
| f51eea6c0f | |||
| befb377774 | |||
| fad862d618 | |||
| fb8a3f5658 | |||
| f2111a376a | |||
| da03ba11c0 |
@@ -0,0 +1,107 @@
|
|||||||
|
name: Android production release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-build-publish:
|
||||||
|
runs-on: godot
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Verify tag belongs to main
|
||||||
|
run: |
|
||||||
|
git fetch origin main
|
||||||
|
git merge-base --is-ancestor "${GITHUB_SHA}" origin/main
|
||||||
|
|
||||||
|
- name: Show Godot version
|
||||||
|
run: godot --version
|
||||||
|
|
||||||
|
- name: Import project
|
||||||
|
run: |
|
||||||
|
rm -rf .godot
|
||||||
|
godot --headless --path . --editor --import --quit --verbose
|
||||||
|
|
||||||
|
- name: Run all tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mapfile -t tests < <(find tests -maxdepth 1 -type f -name 'test_*.gd' | sort)
|
||||||
|
test "${#tests[@]}" -gt 0
|
||||||
|
|
||||||
|
for test_file in "${tests[@]}"; do
|
||||||
|
echo "::group::${test_file}"
|
||||||
|
godot --headless --path . --script "res://${test_file}"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Prepare Android signing
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
run: |
|
||||||
|
test -n "${ANDROID_KEYSTORE_BASE64}"
|
||||||
|
printf '%s' "${ANDROID_KEYSTORE_BASE64}" | base64 --decode > "${RUNNER_TEMP}/release.keystore"
|
||||||
|
test -s "${RUNNER_TEMP}/release.keystore"
|
||||||
|
|
||||||
|
- name: Configure release AAB
|
||||||
|
env:
|
||||||
|
RELEASE_VERSION: ${{ github.ref_name }}
|
||||||
|
RELEASE_VERSION_CODE: ${{ github.run_number }}
|
||||||
|
run: |
|
||||||
|
python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
path = Path("export_presets.cfg")
|
||||||
|
text = path.read_text()
|
||||||
|
start = text.index('[preset.1.options]')
|
||||||
|
head, android = text[:start], text[start:]
|
||||||
|
replacements = (
|
||||||
|
(r'gradle_build/use_gradle_build=false', 'gradle_build/use_gradle_build=true'),
|
||||||
|
(r'gradle_build/export_format=0', 'gradle_build/export_format=1'),
|
||||||
|
(r'package/signed=false', 'package/signed=true'),
|
||||||
|
(r'version/code=\d+', f'version/code={100000 + int(os.environ["RELEASE_VERSION_CODE"])}'),
|
||||||
|
(r'version/name="[^"]*"', f'version/name="{os.environ["RELEASE_VERSION"].removeprefix("v")}"'),
|
||||||
|
)
|
||||||
|
for pattern, replacement in replacements:
|
||||||
|
android, count = re.subn(pattern, replacement, android, count=1)
|
||||||
|
if count != 1:
|
||||||
|
raise RuntimeError(f'Expected one Android preset match for {pattern!r}, got {count}')
|
||||||
|
path.write_text(head + android)
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Build signed Android App Bundle
|
||||||
|
env:
|
||||||
|
GODOT_ANDROID_KEYSTORE_RELEASE_USER: ${{ secrets.ANDROID_KEYSTORE_ALIAS }}
|
||||||
|
GODOT_ANDROID_KEYSTORE_RELEASE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
export GODOT_ANDROID_KEYSTORE_RELEASE_PATH="${RUNNER_TEMP}/release.keystore"
|
||||||
|
mkdir -p build
|
||||||
|
godot --headless --path . --export-release "Android" build/owniverse-${GITHUB_REF_NAME}.aab
|
||||||
|
test -s "build/owniverse-${GITHUB_REF_NAME}.aab"
|
||||||
|
ls -lh "build/owniverse-${GITHUB_REF_NAME}.aab"
|
||||||
|
|
||||||
|
- name: Publish to Google Play production
|
||||||
|
uses: r0adkll/upload-google-play@v1
|
||||||
|
with:
|
||||||
|
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||||
|
packageName: com.KIDGameProduction.Owniverse
|
||||||
|
releaseFiles: build/owniverse-${{ github.ref_name }}.aab
|
||||||
|
tracks: production
|
||||||
|
status: completed
|
||||||
|
|
||||||
|
- name: Upload release bundle
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: owniverse-${{ github.ref_name }}-aab
|
||||||
|
path: build/owniverse-${{ github.ref_name }}.aab
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 90
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
name: Android
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
android:
|
|
||||||
runs-on: godot
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: true
|
|
||||||
- name: Godot version
|
|
||||||
run: godot --version
|
|
||||||
|
|
||||||
- name: Prepare Godot
|
|
||||||
run: |
|
|
||||||
rm -rf .godot
|
|
||||||
|
|
||||||
godot \
|
|
||||||
--headless \
|
|
||||||
--path . \
|
|
||||||
--editor \
|
|
||||||
--import \
|
|
||||||
--quit \
|
|
||||||
--verbose
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build APK
|
|
||||||
run: |
|
|
||||||
mkdir -p build
|
|
||||||
|
|
||||||
godot \
|
|
||||||
--headless \
|
|
||||||
--path . \
|
|
||||||
--editor \
|
|
||||||
--import \
|
|
||||||
--quit
|
|
||||||
|
|
||||||
godot \
|
|
||||||
--headless \
|
|
||||||
--path . \
|
|
||||||
--export-release "Android" \
|
|
||||||
build/app.apk
|
|
||||||
|
|
||||||
|
|
||||||
- name: Check imported resources
|
|
||||||
run: |
|
|
||||||
echo "=== PNG imports ==="
|
|
||||||
find .godot/imported -iname '*habitable*' -o -iname '*civ2*'
|
|
||||||
|
|
||||||
echo "=== PNG files ==="
|
|
||||||
ls -lh ui/4-life/habitable_planet.png
|
|
||||||
ls -lh ui/4-life/civ2.png
|
|
||||||
|
|
||||||
|
|
||||||
- name: Verify APK
|
|
||||||
run: |
|
|
||||||
test -f build/app.apk
|
|
||||||
ls -lh build/app.apk
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
run: |
|
|
||||||
curl -f \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases" \
|
|
||||||
-d "{
|
|
||||||
\"tag_name\": \"${GITHUB_REF_NAME}\",
|
|
||||||
\"name\": \"${GITHUB_REF_NAME}\",
|
|
||||||
\"body\": \"Android build ${GITHUB_REF_NAME}\"
|
|
||||||
}"
|
|
||||||
|
|
||||||
- name: Upload APK to Release
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
run: |
|
|
||||||
RELEASE_ID=$(curl -fsS \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases/tags/${GITHUB_REF_NAME}" \
|
|
||||||
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
|
||||||
|
|
||||||
curl -f \
|
|
||||||
-X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-F "attachment=@build/app.apk" \
|
|
||||||
"https://gt.owad.ru/api/v1/repos/K/owniverse/releases/${RELEASE_ID}/assets?name=owniverse-${GITHUB_REF_NAME}.apk"
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: Testing builds
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- testing
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-and-build:
|
||||||
|
runs-on: godot
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Show Godot version
|
||||||
|
run: godot --version
|
||||||
|
|
||||||
|
- name: Import project
|
||||||
|
run: |
|
||||||
|
rm -rf .godot
|
||||||
|
godot --headless --path . --editor --import --quit --verbose
|
||||||
|
|
||||||
|
- name: Run all tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mapfile -t tests < <(find tests -maxdepth 1 -type f -name 'test_*.gd' | sort)
|
||||||
|
test "${#tests[@]}" -gt 0
|
||||||
|
|
||||||
|
for test_file in "${tests[@]}"; do
|
||||||
|
echo "::group::${test_file}"
|
||||||
|
godot --headless --path . --script "res://${test_file}"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Build Android APK
|
||||||
|
run: |
|
||||||
|
mkdir -p build
|
||||||
|
godot --headless --path . --export-release "Android" build/owniverse-testing.apk
|
||||||
|
|
||||||
|
- name: Build Windows EXE
|
||||||
|
run: |
|
||||||
|
godot --headless --path . --export-release "Windows Desktop" build/owniverse-testing.exe
|
||||||
|
|
||||||
|
- name: Verify builds
|
||||||
|
run: |
|
||||||
|
test -s build/owniverse-testing.apk
|
||||||
|
test -s build/owniverse-testing.exe
|
||||||
|
ls -lh build/owniverse-testing.apk build/owniverse-testing.exe
|
||||||
|
|
||||||
|
- name: Upload testing builds
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: owniverse-testing-${{ github.sha }}
|
||||||
|
path: |
|
||||||
|
build/owniverse-testing.apk
|
||||||
|
build/owniverse-testing.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
+6
-27
@@ -1,4 +1,5 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
const SPEED_LIMIT: int = 6
|
const SPEED_LIMIT: int = 6
|
||||||
|
|
||||||
@@ -57,37 +58,15 @@ func get_description(item_id: String, items: Array = []) -> String:
|
|||||||
return render_template(row, variables)
|
return render_template(row, variables)
|
||||||
|
|
||||||
|
|
||||||
func format_number(value: float, digits: int = 0) -> String:
|
func format_number(value, digits: int = 0) -> String:
|
||||||
if value == 0: return ""
|
return E.grouped(value, digits)
|
||||||
# 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: String = ""
|
|
||||||
if integer.begins_with("-") or integer.begins_with("+"):
|
|
||||||
sign = integer.left(1)
|
|
||||||
integer = integer.substr(1)
|
|
||||||
var grouped: String = ""
|
|
||||||
while integer.length() > 3:
|
|
||||||
grouped = " " + integer.right(3) + grouped
|
|
||||||
integer = integer.left(integer.length() - 3)
|
|
||||||
grouped = sign + integer + grouped
|
|
||||||
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 _ready() -> void:
|
func _ready() -> void:
|
||||||
CivilizationRules.clear()
|
CivilizationRules.clear()
|
||||||
for row: Dictionary in _read_from_csv("res://data/civilization_rules.txt").values():
|
for row: Dictionary in _read_from_csv("res://data/civilization_rules.txt").values():
|
||||||
if not CivilizationRules.has(row.stage):
|
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"}
|
"colonizes": row.colonizes == "1", "fights": row.fights == "1", "diplomacy": row.diplomacy == "1", "worships": row.worships == "1"}
|
||||||
CivilizationRules[row.stage].ethics[row.ethic] = row
|
CivilizationRules[row.stage].ethics[row.ethic] = row
|
||||||
var stages: Array = CivilizationRules.keys()
|
var stages: Array = CivilizationRules.keys()
|
||||||
@@ -104,8 +83,8 @@ func _ready() -> void:
|
|||||||
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
|
Locales["en"] = _read_from_csv("res://data/locale.en.txt")
|
||||||
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
|
Locales["ru"] = _read_from_csv("res://data/locale.ru.txt")
|
||||||
|
|
||||||
Templates["en"] = _read_from_csv("res://data/templates.en.txt")
|
|
||||||
Templates["ru"] = _read_from_csv("res://data/templates.ru.txt")
|
Templates = OwniverseTemplate.get_data()
|
||||||
|
|
||||||
|
|
||||||
func _read_from_csv(file_path: String) -> Dictionary:
|
func _read_from_csv(file_path: String) -> Dictionary:
|
||||||
|
|||||||
+154
-135
@@ -1,18 +1,19 @@
|
|||||||
# SaveManager.gd (добавить в AutoLoad)
|
# SaveManager.gd (добавить в AutoLoad)
|
||||||
extends Node
|
extends Node
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
|
# ~/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 SAVE_PATH = "user://settings.cfg"
|
||||||
|
|
||||||
const FORMAT_VERSION: int = 2
|
const FORMAT_VERSION : int = 5
|
||||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
const STATE_VERSION: String = SimulationTime.STATE_VERSION
|
const STATE_VERSION : String = SimulationTime.STATE_VERSION
|
||||||
var last_error: String = ""
|
var last_error: String = ""
|
||||||
var blocked_saves: Dictionary = {}
|
var blocked_saves: Dictionary = {}
|
||||||
var save_directory: String = "user://"
|
var save_directory: String = "user://"
|
||||||
signal save_completed(filename: String, success: bool, error: String)
|
signal save_completed ( filename : String , success : bool , error : String )
|
||||||
signal saves_drained
|
signal saves_drained
|
||||||
|
|
||||||
# Queue ownership stays on the main thread. The worker only sees its snapshot.
|
# Queue ownership stays on the main thread. The worker only sees its snapshot.
|
||||||
@@ -21,30 +22,28 @@ var _active_filename: String = ""
|
|||||||
var _pending_saves: Dictionary = {}
|
var _pending_saves: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: Variant) -> void:
|
||||||
if _save_thread != null and not _save_thread.is_alive():
|
if (E.ne(_save_thread, null) and (not _save_thread.is_alive())):
|
||||||
_finish_save()
|
_finish_save()
|
||||||
|
|
||||||
|
|
||||||
func is_saving() -> bool:
|
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:
|
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.format_version = FORMAT_VERSION
|
||||||
payload.app_version = str(ProjectSettings.get_setting("application/config/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,
|
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(), "entities": Global.OwniverseEntities.keys()}
|
||||||
"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.
|
## True means queued, not written. Completion is reported by save_completed.
|
||||||
func request_save(filename: String, password: String, data: Dictionary) -> bool:
|
func request_save(filename: String, password: String, data: Dictionary) -> bool:
|
||||||
if blocked_saves.has(filename):
|
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)
|
_pending_saves[filename] = _snapshot(filename, password, data)
|
||||||
if _save_thread == null:
|
if E.eq(_save_thread, null):
|
||||||
_start_save()
|
_start_save()
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -57,12 +56,12 @@ func _start_save() -> void:
|
|||||||
_pending_saves.erase(_active_filename)
|
_pending_saves.erase(_active_filename)
|
||||||
_save_thread = Thread.new()
|
_save_thread = Thread.new()
|
||||||
var error: Error = _save_thread.start(_write_snapshot.bind(job))
|
var error: Error = _save_thread.start(_write_snapshot.bind(job))
|
||||||
if error != OK:
|
if E.ne(error, OK):
|
||||||
_save_thread = null
|
_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)
|
save_completed.emit(_active_filename, false, last_error)
|
||||||
_start_save()
|
_start_save()
|
||||||
if not is_saving(): saves_drained.emit()
|
if (not is_saving()): saves_drained.emit()
|
||||||
|
|
||||||
|
|
||||||
func _finish_save() -> void:
|
func _finish_save() -> void:
|
||||||
@@ -70,17 +69,17 @@ func _finish_save() -> void:
|
|||||||
var filename: String = _active_filename
|
var filename: String = _active_filename
|
||||||
_save_thread = null
|
_save_thread = null
|
||||||
last_error = error
|
last_error = error
|
||||||
if error != "": push_error(error)
|
if E.ne(error, ""): push_error(error)
|
||||||
save_completed.emit(filename, error == "", error)
|
save_completed.emit(filename, E.eq(error, ""), error)
|
||||||
if _save_thread == null: _start_save()
|
if E.eq(_save_thread, null): _start_save()
|
||||||
if not is_saving(): saves_drained.emit()
|
if (not is_saving()): saves_drained.emit()
|
||||||
|
|
||||||
|
|
||||||
## Used for application suspension and teardown, when frames may stop arriving.
|
## Used for application suspension and teardown, when frames may stop arriving.
|
||||||
func flush_saves() -> void:
|
func flush_saves() -> void:
|
||||||
while is_saving():
|
while is_saving():
|
||||||
if _save_thread == null: _start_save()
|
if E.eq(_save_thread, null): _start_save()
|
||||||
if _save_thread != null: _finish_save()
|
if E.ne(_save_thread, null): _finish_save()
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
@@ -99,44 +98,44 @@ func save_data(filename: String, password: String, data: Dictionary) -> bool:
|
|||||||
flush_saves()
|
flush_saves()
|
||||||
last_error = ""
|
last_error = ""
|
||||||
if blocked_saves.has(filename):
|
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))
|
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:
|
static func _write_snapshot(job: Dictionary) -> String:
|
||||||
var payload: Dictionary = job.payload
|
var payload: Dictionary = job.payload
|
||||||
var problem: String = _validate_payload(payload, job.speed_limit, job.stages)
|
var problem: String = _validate_payload(payload, job.speed_limit, job.stages, job.entities)
|
||||||
if problem != "":
|
if E.ne(problem, ""):
|
||||||
return "Invalid save: " + problem
|
return E.add("Invalid save: ", problem)
|
||||||
var path: String = job.path
|
var path: String = job.path
|
||||||
var temporary: String = path + ".tmp"
|
var temporary: String = E.add(path, ".tmp")
|
||||||
var file := FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
var file = FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
||||||
if file == null:
|
if E.eq(file, null):
|
||||||
return "Cannot write encrypted save: " + temporary
|
return E.add("Cannot write encrypted save: ", temporary)
|
||||||
file.store_var(payload, false)
|
file.store_var(payload, false)
|
||||||
file.close()
|
file.close()
|
||||||
# Check the actual encrypted file, including finalization, before replacing the old save.
|
# Check the actual encrypted file, including finalization, before replacing the old save.
|
||||||
var verification := FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, job.password)
|
var verification = FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, job.password)
|
||||||
if verification == null:
|
if E.eq(verification, null):
|
||||||
return "Cannot verify encrypted save: " + temporary
|
return E.add("Cannot verify encrypted save: ", temporary)
|
||||||
var restored: Variant = verification.get_var(false)
|
var restored: Variant = verification.get_var(false)
|
||||||
verification.close()
|
verification.close()
|
||||||
if not restored is Dictionary or restored != payload:
|
if ((not (restored is Dictionary)) or E.ne(restored, payload)):
|
||||||
return "Encrypted save verification failed: " + temporary
|
return E.add("Encrypted save verification failed: ", temporary)
|
||||||
if DirAccess.rename_absolute(temporary, path) != OK:
|
if E.ne(DirAccess.rename_absolute(temporary, path), OK):
|
||||||
return "Cannot replace encrypted save: " + path
|
return E.add("Cannot replace encrypted save: ", path)
|
||||||
if job.plaintext:
|
if job.plaintext:
|
||||||
var plain_path: String = path + ".json"
|
var plain_path: String = E.add(path, ".json")
|
||||||
file = FileAccess.open(plain_path + ".tmp", FileAccess.WRITE)
|
file = FileAccess.open(E.add(plain_path, ".tmp"), FileAccess.WRITE)
|
||||||
if file == null:
|
if E.eq(file, null):
|
||||||
return "Encrypted save written, but cannot write diagnostic copy: " + plain_path
|
return E.add("Encrypted save written, but cannot write diagnostic copy: ", plain_path)
|
||||||
file.store_string(JSON.stringify(payload, " ", true, true) + "\n")
|
file.store_string(E.add(JSON.stringify(payload, " ", true, true), "\n"))
|
||||||
file.flush()
|
file.flush()
|
||||||
var write_error: Error = file.get_error()
|
var write_error: Error = file.get_error()
|
||||||
file.close()
|
file.close()
|
||||||
if write_error != OK or DirAccess.rename_absolute(plain_path + ".tmp", plain_path) != OK:
|
if (E.ne(write_error, OK) or E.ne(DirAccess.rename_absolute(E.add(plain_path, ".tmp"), plain_path), OK)):
|
||||||
return "Encrypted save written, but diagnostic copy failed: " + plain_path
|
return E.add("Encrypted save written, but diagnostic copy failed: ", plain_path)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
@@ -144,174 +143,194 @@ func load_data(filename: String, password: String) -> Dictionary:
|
|||||||
flush_saves()
|
flush_saves()
|
||||||
last_error = ""
|
last_error = ""
|
||||||
# Plaintext is diagnostic only: never use it as a fallback.
|
# Plaintext is diagnostic only: never use it as a fallback.
|
||||||
var path: String = save_directory.path_join(filename + ".adventure")
|
var path: String = save_directory.path_join(E.add(filename, ".adventure"))
|
||||||
if not FileAccess.file_exists(path):
|
if (not FileAccess.file_exists(path)):
|
||||||
return {}
|
return {}
|
||||||
var file := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
var file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
||||||
if file == null:
|
if E.eq(file, null):
|
||||||
blocked_saves[filename] = true
|
blocked_saves[filename] = true
|
||||||
_fail("Cannot decrypt save: " + path)
|
_fail(E.add("Cannot decrypt save: ", path))
|
||||||
return {}
|
return {}
|
||||||
var data: Variant = file.get_var(false)
|
var data: Variant = file.get_var(false)
|
||||||
file.close()
|
file.close()
|
||||||
# Unsupported formats are archived, never converted or overwritten.
|
# Unsupported formats are archived, never converted or overwritten.
|
||||||
# Returning no data lets the stage start a new, saveable session.
|
# Returning no data lets the stage start a new, saveable session.
|
||||||
if data is Dictionary and data.get("format_version") != FORMAT_VERSION:
|
if ((data is Dictionary) and E.ne(data.get("format_version"), FORMAT_VERSION)):
|
||||||
var archive: String = path + ".incompatible.bak"
|
var archive: String = E.add(path, ".incompatible.bak")
|
||||||
var index: int = 0
|
var index: int = 0
|
||||||
while FileAccess.file_exists(archive):
|
while FileAccess.file_exists(archive):
|
||||||
index += 1
|
index = E.add(index, 1)
|
||||||
archive = path + ".incompatible.bak." + str(index)
|
archive = E.add(E.add(path, ".incompatible.bak."), str(index))
|
||||||
var error: Error = DirAccess.rename_absolute(path, archive)
|
var error: Error = DirAccess.rename_absolute(path, archive)
|
||||||
if error != OK:
|
if E.ne(error, OK):
|
||||||
blocked_saves[filename] = true
|
blocked_saves[filename] = true
|
||||||
_fail("Cannot archive incompatible save: " + error_string(error))
|
_fail(E.add("Cannot archive incompatible save: ", error_string(error)))
|
||||||
return {}
|
return {}
|
||||||
blocked_saves.erase(filename)
|
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 {}
|
return {}
|
||||||
var problem: String = validate_data(data)
|
var problem: String = validate_data(data)
|
||||||
if problem != "":
|
if E.ne(problem, ""):
|
||||||
blocked_saves[filename] = true
|
blocked_saves[filename] = true
|
||||||
_fail("Invalid save " + path + ": " + problem)
|
_fail(E.add(E.add(E.add("Invalid save ", path), ": "), problem))
|
||||||
return {}
|
return {}
|
||||||
blocked_saves.erase(filename)
|
blocked_saves.erase(filename)
|
||||||
return data
|
return E.decode(data)
|
||||||
|
|
||||||
|
|
||||||
static func _number(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
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 E.numeric(value) and E.finite(value) and (not value is float or absf(value) < 1e28) and (not whole or E.eq(value, E.floor_value(value))) and (not nonnegative or E.ge(value, 0))
|
||||||
return false
|
|
||||||
return (not whole or float(value) == floorf(float(value))) and (not nonnegative or value >= 0)
|
|
||||||
|
|
||||||
|
|
||||||
static func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
static func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||||
if not value is Dictionary:
|
if (not (value is Dictionary)):
|
||||||
return false
|
return false
|
||||||
for key in value:
|
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 false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
static func _plain_tree(value: Variant) -> bool:
|
static func _plain_tree(value: Variant) -> bool:
|
||||||
if value is Dictionary:
|
if (value is Dictionary):
|
||||||
for key in value:
|
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 false
|
||||||
return true
|
return true
|
||||||
if value is Array:
|
if (value is Array):
|
||||||
for item in value:
|
for item in value:
|
||||||
if not _plain_tree(item):
|
if (not _plain_tree(item)):
|
||||||
return false
|
return false
|
||||||
return true
|
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.
|
## Return a diagnostic path on failure; an empty string means the structure is valid.
|
||||||
func validate_data(data: Variant) -> String:
|
func validate_data(data: Variant) -> String:
|
||||||
return _validate_payload(data, Global.SPEED_LIMIT, Global.CivilizationRules.keys())
|
return _validate_payload(data, Global.SPEED_LIMIT, Global.CivilizationRules.keys(), Global.OwniverseEntities.keys())
|
||||||
|
|
||||||
|
|
||||||
static func _validate_payload(data: Variant, speed_limit: int, stages: Array) -> String:
|
static func _validate_payload(data: Variant, speed_limit: int, stages: Array, entities: 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"
|
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"
|
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 E.numeric(data.ui.get("current_page")))) or E.lt(data.ui.current_page, 0)):
|
||||||
return "invalid ui.current_page"
|
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"
|
return "state must be a Dictionary"
|
||||||
var saved: Dictionary = data.state
|
var saved: Dictionary = data.state
|
||||||
var required := [
|
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"]
|
||||||
"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
|
# Optional additions to the current format. Saves predating indicators keep
|
||||||
# their quantities and receive neutral defaults from Owniverse on load.
|
# their quantities and receive neutral defaults from Owniverse on load.
|
||||||
for field in saved:
|
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"
|
return "unsupported state structure"
|
||||||
if saved.has("world_parameters"):
|
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"
|
return "invalid state.world_parameters"
|
||||||
for key in WorldParameters.defaults():
|
for key in WorldParameters.defaults():
|
||||||
if not saved.world_parameters.has(key) or saved.world_parameters[key] < 1.0:
|
if ((not saved.world_parameters.has(key)) or E.lt(saved.world_parameters[key], 1)):
|
||||||
return "invalid world parameter " + key
|
return E.add("invalid world parameter ", key)
|
||||||
if key in ["Bur", "Exp", "Agg"] and saved.world_parameters[key] > 19.0:
|
if (E.contains(["Bur", "Exp", "Agg"], key) and E.gt(saved.world_parameters[key], 19)):
|
||||||
return "invalid world scale " + key
|
return E.add("invalid world scale ", key)
|
||||||
if saved.has("trends"):
|
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"
|
return "invalid state.trends"
|
||||||
for key in WorldParameters.TRENDS:
|
for key in WorldParameters.TRENDS:
|
||||||
if not saved.trends.has(key) or saved.trends[key] >= 1.0:
|
if ((not saved.trends.has(key)) or E.ge(saved.trends[key], 1)):
|
||||||
return "invalid trend " + key
|
return E.add("invalid trend ", key)
|
||||||
for field in required:
|
for field in required:
|
||||||
if not saved.has(field):
|
if (not saved.has(field)):
|
||||||
return "missing state." + field
|
return E.add("missing state.", field)
|
||||||
if saved.version != STATE_VERSION:
|
if E.ne(saved.version, STATE_VERSION):
|
||||||
return "unsupported state.version"
|
return "unsupported state.version"
|
||||||
if not saved.time is Dictionary or not _number(saved.time.get("datetime")) or not _number(saved.time.get("year")):
|
if (((not (saved.time is Dictionary)) or (not _number(saved.time.get("datetime")))) or (not _number(saved.time.get("year")))):
|
||||||
return "invalid state.time"
|
return "invalid state.time"
|
||||||
if not is_finite(float(saved.time.datetime) * 10.0) or saved.time.year != SimulationTime.processed_year(saved.time.datetime):
|
if E.ne(saved.time.year, SimulationTime.processed_year(saved.time.datetime)):
|
||||||
return "invalid processed time"
|
return "invalid processed time"
|
||||||
if not _number(saved.speed, true) or saved.speed < 1 or saved.speed > speed_limit:
|
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"
|
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"
|
return "invalid state.speed_in_year"
|
||||||
if not _number(saved.energy, true) or not _number_map(saved.amount, true):
|
if data.has("simulation"):
|
||||||
|
var checkpoint: Variant = data.simulation
|
||||||
|
if not checkpoint is Dictionary or not checkpoint.get("productions") is Array:
|
||||||
|
return "invalid simulation checkpoint"
|
||||||
|
if not _number(checkpoint.get("speed"), true) or E.lt(checkpoint.speed, 1) or E.gt(checkpoint.speed, speed_limit):
|
||||||
|
return "invalid simulation speed"
|
||||||
|
for name in ["requested_clock", "active_clock"]:
|
||||||
|
var clock: Variant = checkpoint.get(name)
|
||||||
|
if not clock is Dictionary: return "invalid simulation clock"
|
||||||
|
if clock.is_empty(): continue
|
||||||
|
if not _number(clock.get("datetime")) or not _number(clock.get("year")):
|
||||||
|
return "invalid simulation clock"
|
||||||
|
if E.lt(clock.datetime, saved.time.datetime) or E.ne(clock.year, SimulationTime.processed_year(clock.datetime)):
|
||||||
|
return "invalid simulation clock"
|
||||||
|
if not E.finite(clock.get("tick_remainder", 0)) or E.ge(E.absolute(clock.get("tick_remainder", 0)), 1):
|
||||||
|
return "invalid simulation tick"
|
||||||
|
if not checkpoint.active_clock.is_empty():
|
||||||
|
if checkpoint.requested_clock.is_empty() or E.gt(checkpoint.active_clock.datetime, checkpoint.requested_clock.datetime):
|
||||||
|
return "invalid simulation clock order"
|
||||||
|
for command in checkpoint.productions:
|
||||||
|
if not command is Dictionary or not entities.has(command.get("id")) or not command.get("resource") is bool or not _number(command.get("amount")):
|
||||||
|
return "invalid queued production"
|
||||||
|
if command.resource and command.id not in ["S", "H"]: return "invalid queued resource production"
|
||||||
|
if ((not _number(saved.energy, true)) or (not _number_map(saved.amount, true))):
|
||||||
return "invalid state resources"
|
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"
|
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"
|
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"
|
return "state.binds and state.batches must be Dictionaries"
|
||||||
for owner_id in saved.binds:
|
for owner_id in saved.binds:
|
||||||
if not saved.amount.has(owner_id) or not _number_map(saved.binds[owner_id], true):
|
if ((not saved.amount.has(owner_id)) or (not _number_map(saved.binds[owner_id], true))):
|
||||||
return "invalid state.binds." + owner_id
|
return E.add("invalid state.binds.", owner_id)
|
||||||
for id in saved.binds[owner_id]:
|
for id in saved.binds[owner_id]:
|
||||||
if not saved.amount.has(id):
|
if (not saved.amount.has(id)):
|
||||||
return "unknown bound resource " + id
|
return E.add("unknown bound resource ", id)
|
||||||
for id in saved.batches:
|
for id in saved.batches:
|
||||||
if not saved.amount.has(id) or not saved.batches[id] is Array:
|
if ((not saved.amount.has(id)) or (not (saved.batches[id] is Array))):
|
||||||
return "invalid state.batches." + id
|
return E.add("invalid state.batches.", id)
|
||||||
for batch in saved.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")):
|
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
|
return E.add("invalid batch in ", id)
|
||||||
if batch.end_year < batch.start_year:
|
if E.lt(batch.end_year, batch.start_year):
|
||||||
return "reversed batch interval in " + id
|
return E.add("reversed batch interval in ", id)
|
||||||
if batch.has("created_from") != batch.has("created_to"):
|
if E.ne(batch.has("created_from"), batch.has("created_to")):
|
||||||
return "incomplete batch creation interval in " + id
|
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 batch.created_to <= batch.created_from or batch.start_year < batch.created_from):
|
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 "invalid batch creation interval in " + id
|
return E.add("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):
|
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 "invalid batch.processed_until in " + id
|
return E.add("invalid batch.processed_until in ", id)
|
||||||
if batch.has("withdraw_remainder") and (not _number(batch.withdraw_remainder) or batch.withdraw_remainder > batch.amount):
|
if (batch.has("withdraw_remainder") and (((not _number(batch.withdraw_remainder)) or E.gt(batch.withdraw_remainder, batch.amount)))):
|
||||||
return "invalid batch.withdraw_remainder in " + id
|
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"]:
|
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):
|
if (not _number_map(saved[field], false, false)):
|
||||||
return "invalid state." + field
|
return E.add("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.auto_resources, true)) or (not _number_map(saved.auto_star_deficits, false, false))):
|
||||||
return "invalid automatic production state"
|
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"
|
return "invalid state.resource_remainders"
|
||||||
for channel in ["basic", "automatic"]:
|
for channel in ["basic", "automatic"]:
|
||||||
if not _number_map(saved.resource_remainders[channel], false, false):
|
if (not _number_map(saved.resource_remainders[channel], false, false)):
|
||||||
return "invalid state.resource_remainders." + channel
|
return E.add("invalid state.resource_remainders.", channel)
|
||||||
if not saved.ethics is Dictionary or not saved.civilization_homes is Dictionary:
|
if ((not (saved.ethics is Dictionary)) or (not (saved.civilization_homes is Dictionary))):
|
||||||
return "invalid civilization state"
|
return "invalid civilization state"
|
||||||
for id in saved.ethics:
|
for id in saved.ethics:
|
||||||
if id not in stages or not _number_map(saved.ethics[id], true):
|
if (not E.contains(stages, id) or (not _number_map(saved.ethics[id], true))):
|
||||||
return "invalid state.ethics." + str(id)
|
return E.add("invalid state.ethics.", str(id))
|
||||||
for ethic in saved.ethics[id]:
|
for ethic in saved.ethics[id]:
|
||||||
if ethic not in ["m", "l", "w", "d", "c", "b"]:
|
if not E.contains(["m", "l", "w", "d", "c", "b"], ethic):
|
||||||
return "invalid ethic " + str(ethic)
|
return E.add("invalid ethic ", str(ethic))
|
||||||
for id in saved.civilization_homes:
|
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 "invalid civilization homes"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -322,7 +341,7 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array) ->
|
|||||||
#region Settings
|
#region Settings
|
||||||
func save_settings():
|
func save_settings():
|
||||||
var config = ConfigFile.new()
|
var config = ConfigFile.new()
|
||||||
|
|
||||||
for item in Global.Settings:
|
for item in Global.Settings:
|
||||||
config.set_value(SETTING_SECTION, item, Global.Settings[item])
|
config.set_value(SETTING_SECTION, item, Global.Settings[item])
|
||||||
config.save(SAVE_PATH)
|
config.save(SAVE_PATH)
|
||||||
@@ -330,7 +349,7 @@ func save_settings():
|
|||||||
func load_settings():
|
func load_settings():
|
||||||
var config = ConfigFile.new()
|
var config = ConfigFile.new()
|
||||||
var err = config.load(SAVE_PATH)
|
var err = config.load(SAVE_PATH)
|
||||||
if err != OK:
|
if E.ne(err, OK):
|
||||||
return false
|
return false
|
||||||
for item in Global.Settings:
|
for item in Global.Settings:
|
||||||
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
class_name CSVLoader
|
||||||
|
|
||||||
|
const SPLITTER = '|'
|
||||||
|
|
||||||
|
static func load(csv_text: String) -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
|
||||||
|
var lines := csv_text.strip_edges().split("\n")
|
||||||
|
|
||||||
|
if lines.size() < 2:
|
||||||
|
return result
|
||||||
|
|
||||||
|
var headers := lines[0].split(SPLITTER)
|
||||||
|
|
||||||
|
for i in range(1, lines.size()):
|
||||||
|
var line := lines[i].strip_edges()
|
||||||
|
|
||||||
|
if line.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
var values := line.split(SPLITTER)
|
||||||
|
|
||||||
|
if values.size() != headers.size():
|
||||||
|
push_warning(
|
||||||
|
"CSV row %d has %d columns, expected %d"
|
||||||
|
% [i + 1, values.size(), headers.size()]
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var elem: Dictionary = {}
|
||||||
|
|
||||||
|
for j in range(headers.size()):
|
||||||
|
elem[headers[j].strip_edges()] = values[j].strip_edges()
|
||||||
|
|
||||||
|
result[elem[headers[0]]] = elem
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bfe1fyutcw0cx
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
uid://cunl2sqmcj1ff
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="keep"
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[remap]
|
|
||||||
|
|
||||||
importer="skip"
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[gd_resource type="AudioBusLayout" format=3 uid="uid://b5g7p85k5do0m"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
bus/1/name = &"Music"
|
||||||
|
bus/1/solo = false
|
||||||
|
bus/1/mute = false
|
||||||
|
bus/1/bypass_fx = false
|
||||||
|
bus/1/volume_db = 0.0
|
||||||
|
bus/1/send = &"Master"
|
||||||
|
bus/2/name = &"FX"
|
||||||
|
bus/2/solo = false
|
||||||
|
bus/2/mute = false
|
||||||
|
bus/2/bypass_fx = false
|
||||||
|
bus/2/volume_db = 0.0
|
||||||
|
bus/2/send = &"Master"
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
# Эволюция звёзд Owniverse.GI
|
# Эволюция звёзд Owniverse.GI
|
||||||
|
|
||||||
|
Числа симуляции хранят целую часть в двух блоках int64, дробный остаток — в
|
||||||
|
float. Весь порядок E27 сохраняет целые единицы. Возраст разделён на целые
|
||||||
|
годы и дробную часть с шагом 10^-9 года; большие промежуточные произведения
|
||||||
|
распределений вычисляются через `mul_div`. Границы точности, допуск переноса
|
||||||
|
дробей и несовместимый со старым формат сохранения 4 описаны в
|
||||||
|
[документации математики](world/owniverse/math/README.md).
|
||||||
|
|
||||||
Механики перенесены из Owniverse.U с согласованными изменениями GI: постепенные партии, детерминированные дробные остатки, уязвимость цивилизаций до C1.2 и передача выживших планет преемнику. Баланс таблицы GI сохраняется.
|
Механики перенесены из Owniverse.U с согласованными изменениями GI: постепенные партии, детерминированные дробные остатки, уязвимость цивилизаций до C1.2 и передача выживших планет преемнику. Баланс таблицы GI сохраняется.
|
||||||
|
|
||||||
## Порядок расчёта
|
## Порядок расчёта
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+81
-96
@@ -1,4 +1,5 @@
|
|||||||
extends Control
|
extends Control
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
|
||||||
|
|
||||||
@@ -90,58 +91,46 @@ var g_counters: Dictionary[String, Counter] = {
|
|||||||
var g_active_items: Dictionary = {}
|
var g_active_items: Dictionary = {}
|
||||||
|
|
||||||
var pending_page: int = -1
|
var pending_page: int = -1
|
||||||
|
var active_actions_dirty: bool = true
|
||||||
|
var active_actions_elapsed: float = 0.0
|
||||||
|
|
||||||
var owniverse:Owniverse = Owniverse.new()
|
var owniverse:Owniverse = Owniverse.new()
|
||||||
var owniverse_items: Dictionary[String, OwniverseItem] = {}
|
var owniverse_items: Dictionary[String, OwniverseItem] = {}
|
||||||
var locked_items: Array = []
|
var locked_items: Array = []
|
||||||
|
|
||||||
|
|
||||||
func set_datetime(value: float):
|
func set_datetime(value):
|
||||||
# Simulation time advances in tenths of a year; displaying five decimal
|
# Simulation time advances in tenths of a year; displaying five decimal
|
||||||
# places only adds zeroes and suggests precision the simulation does not
|
# places only adds zeroes and suggests precision the simulation does not
|
||||||
# have.
|
# 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
|
#region Item manipulations: update and visibility
|
||||||
|
|
||||||
|
|
||||||
func set_owniverse_item_value() -> void:
|
func set_owniverse_item_value() -> void:
|
||||||
for item_id in owniverse.items_to_update:
|
for item_id in owniverse.presentation.pending_amounts:
|
||||||
if owniverse_items.has(item_id):
|
if owniverse_items.has(item_id):
|
||||||
owniverse_items[item_id].set_label(owniverse.items_to_update[item_id])
|
owniverse_items[item_id].set_label(owniverse.presentation.pending_amounts[item_id])
|
||||||
owniverse.items_to_update.clear()
|
owniverse.presentation.pending_amounts.clear()
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
func _unlock_items():
|
func _unlock_items():
|
||||||
if locked_items.size() == 0: return
|
var result: Dictionary = owniverse.presentation.unlocks(owniverse.state, owniverse.catalog, locked_items)
|
||||||
var _new_locked_items = []
|
for item_id in result.visible:
|
||||||
var display_totals: Dictionary = owniverse.get_item_amounts(true)
|
if owniverse_items.has(item_id):
|
||||||
for _item_id in locked_items:
|
owniverse_items[item_id].set_visible(true)
|
||||||
#print(_item_id ' | ', _entity.unlock)
|
locked_items = result.remaining
|
||||||
var _flag_to_unlock = true
|
|
||||||
for _elem in owniverse.entities[_item_id].unlock:
|
|
||||||
if maxf(owniverse.state.stats.total_amount.get(_elem, 0.0), display_totals.get(_elem, 0.0)) < owniverse.entities[_item_id].unlock[_elem]:
|
|
||||||
_flag_to_unlock = false
|
|
||||||
break
|
|
||||||
|
|
||||||
if _flag_to_unlock:
|
|
||||||
for item_id in [_item_id, owniverse.entities[_item_id].count_as]:
|
|
||||||
if owniverse_items.has(item_id):
|
|
||||||
owniverse_items[item_id].set_visible(true)
|
|
||||||
else:
|
|
||||||
_new_locked_items.append(_item_id)
|
|
||||||
locked_items = _new_locked_items
|
|
||||||
|
|
||||||
|
|
||||||
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
func _on_indicator_pressed(indicator_name: String, page: int = 0):
|
||||||
_set_description(indicator_name, page)
|
_set_description(indicator_name, page)
|
||||||
|
|
||||||
|
|
||||||
func _set_item_description(item: OwniverseItem) -> void:
|
func _set_item_description(item: OwniverseItem) -> void:
|
||||||
var category: String = "Entity" if owniverse.entities.has(item.item_id) else "Force"
|
var category: String = "Entity" if owniverse.catalog.entities.has(item.item_id) else "Force"
|
||||||
var id: String = item.item_id if category == "Entity" else item.item_name.capitalize()
|
var id: String = item.item_id if category == "Entity" else item.item_name.capitalize()
|
||||||
var page: int = clampi(item.item_page, 0, 4)
|
var page: int = clampi(item.item_page, 0, 4)
|
||||||
if description_targets.get(page, {}).get("id", "") != id:
|
if description_targets.get(page, {}).get("id", "") != id:
|
||||||
@@ -170,8 +159,8 @@ func _update_descriptions(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
for page in description_targets:
|
for page in description_targets:
|
||||||
var id: String = description_targets[page].id
|
var id: String = description_targets[page].id
|
||||||
var total: float = ItemDescription.quantities(owniverse, id).total
|
var total = ItemDescription.quantities(owniverse, id).total
|
||||||
description_rates[id] = (total - description_samples[id]) / description_elapsed if description_samples.has(id) else 0.0
|
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_samples[id] = total
|
||||||
description_elapsed = 0.0
|
description_elapsed = 0.0
|
||||||
_refresh_descriptions()
|
_refresh_descriptions()
|
||||||
@@ -181,13 +170,14 @@ func _control_page(item: OwniverseItem) -> void:
|
|||||||
# TODO переписать это ↓
|
# TODO переписать это ↓
|
||||||
if !g_active_items.has(item.item_page):
|
if !g_active_items.has(item.item_page):
|
||||||
g_active_items[item.item_page] = null
|
g_active_items[item.item_page] = null
|
||||||
|
|
||||||
var current_item: OwniverseItem = g_active_items[item.item_page]
|
var current_item: OwniverseItem = g_active_items[item.item_page]
|
||||||
if current_item == item:
|
if current_item == item:
|
||||||
return
|
return
|
||||||
|
|
||||||
g_active_items[item.item_page] = item
|
g_active_items[item.item_page] = item
|
||||||
|
active_actions_dirty = true
|
||||||
|
|
||||||
if current_item != null and current_item.item_page == item.item_page:
|
if current_item != null and current_item.item_page == item.item_page:
|
||||||
current_item.set_selection(false)
|
current_item.set_selection(false)
|
||||||
|
|
||||||
@@ -216,21 +206,21 @@ func _init_owniverse_items() -> bool:
|
|||||||
item_pages[page].add_child(item)
|
item_pages[page].add_child(item)
|
||||||
item.init_button(item_props)
|
item.init_button(item_props)
|
||||||
item.item_activated.connect(item_activated)
|
item.item_activated.connect(item_activated)
|
||||||
|
|
||||||
owniverse_items[item_id] = item
|
owniverse_items[item_id] = item
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
func item_activated(item: OwniverseItem) -> void:
|
func item_activated(item: OwniverseItem) -> void:
|
||||||
_control_page(item)
|
_control_page(item)
|
||||||
_set_item_description(item)
|
_set_item_description(item)
|
||||||
|
|
||||||
if item.item_id == "S" or item.item_id == "H":
|
if item.item_id == "S" or item.item_id == "H":
|
||||||
var _value = owniverse.state.amount[item.item_id]
|
var _value = owniverse.state.amount[item.item_id]
|
||||||
_value = 10 * _value if _value > 0 else 1
|
_value = E.multiply(9, _value) if E.gt(_value, 0) else 1
|
||||||
owniverse.produce_hs(item.item_id, _value)
|
owniverse.request_production(item.item_id, _value, true)
|
||||||
|
|
||||||
|
|
||||||
func _check_counters(delta: float) -> Array:
|
func _check_counters(delta: float) -> Array:
|
||||||
var actions: Array = []
|
var actions: Array = []
|
||||||
@@ -238,13 +228,13 @@ func _check_counters(delta: float) -> Array:
|
|||||||
var cnt: Counter = g_counters[k]
|
var cnt: Counter = g_counters[k]
|
||||||
if not cnt.is_active:
|
if not cnt.is_active:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cnt.value += delta
|
cnt.value += delta
|
||||||
if cnt.value >= cnt.sec:
|
if cnt.value >= cnt.sec:
|
||||||
actions.append(k)
|
actions.append(k)
|
||||||
cnt.value = 0
|
cnt.value = 0
|
||||||
cnt.is_active = false
|
cnt.is_active = false
|
||||||
|
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
@@ -259,7 +249,7 @@ func _do_actions(actions: Array) -> void:
|
|||||||
|
|
||||||
func _swipe_to_nearest_page() -> void:
|
func _swipe_to_nearest_page() -> void:
|
||||||
if stage_page_scroller.scroll_horizontal % PAGE_WIDTH == 0: return
|
if stage_page_scroller.scroll_horizontal % PAGE_WIDTH == 0: return
|
||||||
|
|
||||||
var new_page: int = round(1.0 * stage_page_scroller.scroll_horizontal / PAGE_WIDTH)
|
var new_page: int = round(1.0 * stage_page_scroller.scroll_horizontal / PAGE_WIDTH)
|
||||||
|
|
||||||
if g_tween != null:
|
if g_tween != null:
|
||||||
@@ -284,35 +274,35 @@ func _input(event: InputEvent) -> void:
|
|||||||
elif event is InputEventScreenTouch:
|
elif event is InputEventScreenTouch:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
func _set_active_page_actions() -> void:
|
func _set_active_page_actions() -> void:
|
||||||
for pa in page_actions:
|
for pa in page_actions:
|
||||||
if !g_active_items.has(pa): continue
|
if !g_active_items.has(pa): continue
|
||||||
|
|
||||||
var item_id: String = g_active_items[pa].item_id
|
var item_id: String = g_active_items[pa].item_id
|
||||||
var entity: OwniverseEntity = owniverse.entities[item_id]
|
var entity: OwniverseEntity = owniverse.catalog.entities[item_id]
|
||||||
|
|
||||||
#var amount = owniverse.get_available_to_produce(item_id)
|
#var amount = owniverse.get_available_to_produce(item_id)
|
||||||
|
|
||||||
#var amount = owniverse.get_available_to_produce(entity)
|
#var amount = owniverse.get_available_to_produce(entity)
|
||||||
var amount = owniverse.calculate_max_producible(entity)
|
var amount = owniverse.production.calculate_max_producible(entity)
|
||||||
#print(item_id, " : ", amount)
|
#print(item_id, " : ", amount)
|
||||||
if item_id == "SMBH":
|
if item_id == "SMBH":
|
||||||
var preview: Callable = func(requested: float) -> float:
|
var preview: Callable = func(requested):
|
||||||
return owniverse.calculate_max_producible(entity, requested)
|
return owniverse.production.calculate_max_producible(entity, requested)
|
||||||
page_actions[pa].set_actions(item_id, amount, preview)
|
page_actions[pa].set_actions(item_id, amount, preview)
|
||||||
else:
|
else:
|
||||||
page_actions[pa].set_actions(item_id, amount)
|
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):
|
if !g_active_items.has(page_id):
|
||||||
return
|
return
|
||||||
|
|
||||||
var item_id: String = g_active_items[page_id].item_id
|
var item_id: String = g_active_items[page_id].item_id
|
||||||
|
|
||||||
if action_amount > 0:
|
if E.gt(action_amount, 0):
|
||||||
owniverse.produce(item_id, action_amount)
|
owniverse.request_production(item_id, action_amount)
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -324,7 +314,7 @@ func _ready() -> void:
|
|||||||
create_new_universe()
|
create_new_universe()
|
||||||
load_data()
|
load_data()
|
||||||
_refresh_universe_ui()
|
_refresh_universe_ui()
|
||||||
|
|
||||||
g_counters.swipe.sec = 0.2
|
g_counters.swipe.sec = 0.2
|
||||||
|
|
||||||
for pa in page_actions:
|
for pa in page_actions:
|
||||||
@@ -339,8 +329,8 @@ func create_new_universe() -> void:
|
|||||||
owniverse = Owniverse.new()
|
owniverse = Owniverse.new()
|
||||||
var inited_result: Dictionary = init_enitities()
|
var inited_result: Dictionary = init_enitities()
|
||||||
locked_items = inited_result.locked_items
|
locked_items = inited_result.locked_items
|
||||||
owniverse.new_state = owniverse.state
|
|
||||||
for entity in previous.entities.values():
|
for entity in previous.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
previous.free()
|
previous.free()
|
||||||
|
|
||||||
@@ -351,6 +341,8 @@ func create_new_universe() -> void:
|
|||||||
description_samples.clear()
|
description_samples.clear()
|
||||||
description_rates.clear()
|
description_rates.clear()
|
||||||
description_elapsed = 0.0
|
description_elapsed = 0.0
|
||||||
|
active_actions_dirty = true
|
||||||
|
active_actions_elapsed = 0.0
|
||||||
for label in g_item_descriptions:
|
for label in g_item_descriptions:
|
||||||
label.text = ""
|
label.text = ""
|
||||||
for item in owniverse_items.values():
|
for item in owniverse_items.values():
|
||||||
@@ -368,16 +360,16 @@ func create_new_universe() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _refresh_universe_ui() -> void:
|
func _refresh_universe_ui() -> void:
|
||||||
owniverse.refresh_item_amounts()
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
_change_speed(0)
|
_change_speed(0)
|
||||||
set_datetime(owniverse.state.time.year)
|
set_datetime(owniverse.state.time.year)
|
||||||
set_owniverse_item_value()
|
set_owniverse_item_value()
|
||||||
_unlock_items()
|
_unlock_items()
|
||||||
if world_indicators != null: world_indicators.update(owniverse)
|
if world_indicators != null: world_indicators.update(owniverse)
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
|
||||||
owniverse.evolute(delta)
|
owniverse.evolute_frame(delta, get_tree())
|
||||||
indicator_elapsed += delta
|
indicator_elapsed += delta
|
||||||
if indicator_elapsed >= 0.1:
|
if indicator_elapsed >= 0.1:
|
||||||
indicator_elapsed = 0.0
|
indicator_elapsed = 0.0
|
||||||
@@ -385,21 +377,27 @@ func _process(delta: float) -> void:
|
|||||||
_update_descriptions(delta)
|
_update_descriptions(delta)
|
||||||
set_owniverse_item_value()
|
set_owniverse_item_value()
|
||||||
set_datetime(owniverse.state.time.year)
|
set_datetime(owniverse.state.time.year)
|
||||||
|
|
||||||
#owniverse.cycle(delta)
|
#owniverse.cycle(delta)
|
||||||
|
|
||||||
#owniverse.cycle_legacy(year_delta)
|
#owniverse.cycle_legacy(year_delta)
|
||||||
|
|
||||||
var actions = _check_counters(delta)
|
var actions = _check_counters(delta)
|
||||||
_do_actions(actions)
|
_do_actions(actions)
|
||||||
|
# Production limits are expensive (they walk the resource ledger). Refresh
|
||||||
_set_active_page_actions()
|
# them when selection changes and at a modest cadence instead of every
|
||||||
|
# rendered frame. The simulation itself continues to run every frame.
|
||||||
|
active_actions_elapsed += delta
|
||||||
|
if active_actions_dirty or active_actions_elapsed >= 0.1:
|
||||||
|
active_actions_elapsed = 0.0
|
||||||
|
active_actions_dirty = false
|
||||||
|
_set_active_page_actions()
|
||||||
|
|
||||||
|
|
||||||
func _change_speed(delta: int) -> void:
|
func _change_speed(delta: int) -> void:
|
||||||
|
|
||||||
owniverse.speed = owniverse.speed + delta
|
owniverse.speed = owniverse.speed + delta
|
||||||
|
|
||||||
var speed: int = owniverse.speed
|
var speed: int = owniverse.speed
|
||||||
var ind: int = 1
|
var ind: int = 1
|
||||||
for icon in g_speed_icons:
|
for icon in g_speed_icons:
|
||||||
@@ -417,14 +415,14 @@ func _change_speed(delta: int) -> void:
|
|||||||
else:
|
else:
|
||||||
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed - 1))
|
g_time_minus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed - 1))
|
||||||
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
|
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
|
||||||
|
|
||||||
|
|
||||||
func _on_topmenu_button_pressed(screen_name: String) -> void:
|
func _on_topmenu_button_pressed(screen_name: String) -> void:
|
||||||
|
|
||||||
for screen_id in screens:
|
for screen_id in screens:
|
||||||
#print(screen_name, " ←→ ", screen_id)
|
#print(screen_name, " ←→ ", screen_id)
|
||||||
screens[screen_id].visible = screen_id == screen_name
|
screens[screen_id].visible = screen_id == screen_name
|
||||||
|
|
||||||
|
|
||||||
func _on_unlock_and_save_timeout() -> void:
|
func _on_unlock_and_save_timeout() -> void:
|
||||||
save_data()
|
save_data()
|
||||||
@@ -451,9 +449,10 @@ func load_data() -> bool:
|
|||||||
var complete: Dictionary = owniverse.state.duplicate(true)
|
var complete: Dictionary = owniverse.state.duplicate(true)
|
||||||
complete.merge(restored, true)
|
complete.merge(restored, true)
|
||||||
owniverse.state = complete
|
owniverse.state = complete
|
||||||
owniverse.new_state = complete
|
owniverse.restore_frame_checkpoint(game_data.get("simulation", {}))
|
||||||
owniverse.initialize_life_batches()
|
|
||||||
owniverse.refresh_item_amounts()
|
owniverse.evolution.initialize_life_batches()
|
||||||
|
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
|
||||||
pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1)
|
pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1)
|
||||||
_restore_page.call_deferred()
|
_restore_page.call_deferred()
|
||||||
return true
|
return true
|
||||||
@@ -473,6 +472,7 @@ func save_data() -> void:
|
|||||||
page = clampi(roundi(float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH), 0, PAGE_COUNT - 1)
|
page = clampi(roundi(float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH), 0, PAGE_COUNT - 1)
|
||||||
SaveManager.request_save("owniverse", "qwerty", {
|
SaveManager.request_save("owniverse", "qwerty", {
|
||||||
"state": owniverse.state,
|
"state": owniverse.state,
|
||||||
|
"simulation": owniverse.frame_checkpoint(),
|
||||||
"ui": {"current_page": page},
|
"ui": {"current_page": page},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -504,37 +504,22 @@ func init_enitities() -> Dictionary:
|
|||||||
var _result := {
|
var _result := {
|
||||||
'locked_items': [],
|
'locked_items': [],
|
||||||
}
|
}
|
||||||
for entity_id in Global.OwniverseEntities:
|
owniverse.catalog.load_definitions(Global.OwniverseEntities)
|
||||||
var entity = OwniverseEntity.new(Global.OwniverseEntities[entity_id])
|
for entity_id in owniverse.catalog.entities:
|
||||||
owniverse.entities[entity_id] = entity
|
var entity: OwniverseEntity = owniverse.catalog.entities[entity_id]
|
||||||
|
|
||||||
owniverse.state.stats.total_amount[entity_id] = 0
|
owniverse.state.stats.total_amount[entity_id] = 0
|
||||||
owniverse.state.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.state.batches[entity.id] = []
|
||||||
owniverse.add_entity_to_batch(entity_id, 0, 0)
|
owniverse.batches.add_entity_to_batch(entity_id, 0, 0)
|
||||||
if entity.group_as != "":
|
if entity.group_as != "":
|
||||||
owniverse.state.amount[entity.group_as] = 0
|
owniverse.state.amount[entity.group_as] = 0
|
||||||
|
|
||||||
owniverse.state.stats.total_amount[entity.group_as] = 0
|
owniverse.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
|
||||||
if entity.unlock != {}:
|
|
||||||
owniverse.locked_entities.append(entity)
|
|
||||||
_result.locked_items.append(entity.id)
|
|
||||||
|
|
||||||
if entity.product_per_year_type != "":
|
|
||||||
owniverse.auto_production.append(entity)
|
|
||||||
|
|
||||||
# groups
|
if entity.unlock != {}:
|
||||||
if entity.group_as != "":
|
_result.locked_items.append(entity.id)
|
||||||
if !owniverse.entity_groups.has(entity.group_as):
|
|
||||||
owniverse.entity_groups[entity.group_as] = []
|
|
||||||
owniverse.entity_groups[entity.group_as].append(entity_id)
|
|
||||||
|
|
||||||
if entity.event_group != "":
|
|
||||||
if !owniverse.entity_event_groups.has(entity.event_group):
|
|
||||||
owniverse.entity_event_groups[entity.event_group] = []
|
|
||||||
owniverse.entity_event_groups[entity.event_group].append(entity_id)
|
|
||||||
|
|
||||||
return _result
|
return _result
|
||||||
|
|
||||||
|
|||||||
+99
-4
@@ -1,7 +1,52 @@
|
|||||||
# Проверки поэтапного переноса из Owniverse.U
|
# Проверки поэтапного переноса из Owniverse.U
|
||||||
|
|
||||||
|
## Производительность на сохранении
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.gd -- /tmp/save.adventure.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Читает диагностический JSON формата 4 в отдельные экземпляры мира и замеряет
|
||||||
|
шесть последовательных шагов на скоростях 1, 4, 6. Запись сохранений не вызывается.
|
||||||
|
Последним аргументом можно задать длительность шага в реальных секундах
|
||||||
|
(по умолчанию 1/60). Это время расчёта симуляции, без отрисовки интерфейса.
|
||||||
|
|
||||||
|
На диагностической копии с 120 партиями прежняя рациональная арифметика после
|
||||||
|
оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6.
|
||||||
|
После перехода на целые блоки и устранения повторного построения физических
|
||||||
|
количеств повторный замер дал 53–58 мс. При шаге 0,1 секунды: 65–67 мс вместо
|
||||||
|
117–138 мс. Это замеры расчёта на данной
|
||||||
|
копии, а не обещание FPS. Для сравнения старый JSON был преобразован только
|
||||||
|
во временный тестовый файл; в загрузчик игры миграция не добавлена.
|
||||||
|
Разреженные сценарии сравниваются с полным расчётом в `test_short_lived_events.gd`.
|
||||||
|
|
||||||
|
## Целые блоки и дробные остатки
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_exact_math.gd`
|
||||||
|
|
||||||
|
Независимый эталон Python Fraction: 160 пар с произведением внутри E27; целые
|
||||||
|
проверяются точно, дроби — с допуском 10^-12. Также границы int64/E27, сериализация,
|
||||||
|
рецепты с дробными стоимостями, сохранение сумм, время и 100 циклов с меняющимися
|
||||||
|
долями. Все остальные тесты используют числовое сравнение и сериализацию через
|
||||||
|
`E`, а не сравнение ссылок на большие числа. Границы точности и новый формат
|
||||||
|
сохранения описаны в [документации арифметики](../world/owniverse/math/README.md).
|
||||||
|
|
||||||
|
## Модули симуляции
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_module_lifecycle.gd`
|
||||||
|
|
||||||
|
Проверяются отдельные экземпляры модулей и состояния для каждой вселенной, слабая обратная ссылка, освобождение модулей вместе с миром и защита от некорректного delta. Остальные тесты используют прямые API batches, ledger, production, automatic, stellar, life, environment и evolution; временные методы-переадресации Owniverse удалены. Описание границ модулей и порядка фаз: `../world/owniverse/README.md`.
|
||||||
|
|
||||||
|
## Математика распределений и каталог
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_quantity_catalog.gd`
|
||||||
|
|
||||||
|
Проверяются целочисленность и границы распределения, сохранение суммы, разрешение равных остатков, накопление дробей и долгов округления, нормализация весов, размер производственной партии, загрузка каталога и его индексы. Архитектура описана в `../world/owniverse/README.md`. Сохранение игрока не используется.
|
||||||
|
|
||||||
## Иконки стадий жизни
|
## Иконки стадий жизни
|
||||||
|
|
||||||
|
Тест также проверяет модуль `item_presentation.gd`: отсутствие изменений входного состояния и списка закрытых иконок, решения по разблокировке, отложенную и принудительную публикацию количеств.
|
||||||
|
|
||||||
`godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd`
|
`godot --headless --path Owniverse.GI --script res://tests/test_life_icons.gd`
|
||||||
|
|
||||||
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
|
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
|
||||||
@@ -35,7 +80,7 @@ godot --headless --path Owniverse.GI --script res://tests/test_ui_updates.gd
|
|||||||
|
|
||||||
Тест сохранений пишет только в уникальную временную папку `/tmp`, затем удаляет собственные файлы. Проверяются независимость снимка, работа кадров при записи, объединение запросов, диагностическая копия, сохранность предыдущего файла при ошибках и завершение очереди при уничтожении менеджера. Сообщения `Invalid save`, `Save blocked` и `Cannot write` в этом тесте ожидаемы.
|
Тест сохранений пишет только в уникальную временную папку `/tmp`, затем удаляет собственные файлы. Проверяются независимость снимка, работа кадров при записи, объединение запросов, диагностическая копия, сохранность предыдущего файла при ошибках и завершение очереди при уничтожении менеджера. Сообщения `Invalid save`, `Save blocked` и `Cannot write` в этом тесте ожидаемы.
|
||||||
|
|
||||||
Счётчики, время и кнопки производства обновляются раз в 0,1 секунды. Изменения симуляции помечают суммы для пересчёта; сами суммы вычисляются при обновлении интерфейса. Текст счётчиков применяется только к видимым элементам и только при изменении строки. Переход между страницами, выбор элемента и производство вызывают немедленное обновление. Описания сохраняют прежний интервал 0,25 секунды. Тест интерфейса использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
|
Тест интерфейса проверяет передачу количеств из presentation в контролы, очистку очереди после публикации, отложенное и принудительное обновление, производство по нажатию и формат времени. Stage в текущей реализации применяет очередь счётчиков и время каждый кадр; интервал 0,1 секунды используется для индикаторов. Рефакторинг этот порядок не меняет. Тест использует игровую сцену с отключёнными загрузкой, сохранением и автоматической симуляцией, не обращаясь к сохранению игрока.
|
||||||
|
|
||||||
## Время: шаг 0,1 игрового года
|
## Время: шаг 0,1 игрового года
|
||||||
|
|
||||||
@@ -47,14 +92,14 @@ godot --headless --path Owniverse.GI --script res://tests/test_time.gd
|
|||||||
|
|
||||||
Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.
|
Проверяются границы десятых долей года, производство ресурсов на этих границах, накопление дробного остатка, скорости 1–10 000 лет/сек, продолжение после загрузки и недопустимые интервалы.
|
||||||
|
|
||||||
Схема состояния `0.1.1` хранит:
|
Схема состояния `0.3.0` хранит:
|
||||||
|
|
||||||
- `time.datetime` — накопленный возраст, включая необработанную дробную часть;
|
- `time.datetime` — накопленный возраст, включая необработанную дробную часть;
|
||||||
- `time.year` — последний рассчитанный возраст с точностью до десятой доли года.
|
- `time.year` — последний рассчитанный возраст с точностью до десятой доли года.
|
||||||
|
|
||||||
Сохранения используют формат 2 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
|
Сохранения используют формат 4 и актуальную структуру состояния. Проверяются целостность данных, фоновая запись и повторная загрузка; поддержка и миграция прежних форматов отсутствуют.
|
||||||
|
|
||||||
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 2. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
|
Файл с несовместимым номером формата при загрузке переносится в `*.adventure.incompatible.bak` (при совпадении имени добавляется числовой суффикс). Игра начинает новую сессию и может сохранять её в формате 4. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
|
||||||
|
|
||||||
## Согласованные правила эволюции
|
## Согласованные правила эволюции
|
||||||
|
|
||||||
@@ -110,3 +155,53 @@ godot --headless --path Owniverse.GI --script res://tests/test_civilization_evol
|
|||||||
```
|
```
|
||||||
|
|
||||||
Проверяются реальные коэффициенты C0.2, войны и побочный ущерб C2.0, замена планет колониями с сохранением хозяина, дипломатическое влияние, бонус периода от структур и деградация при их отсутствии. Ускоренная модель проходит все стадии C0.2 → C4.4 → Ct. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.
|
Проверяются реальные коэффициенты C0.2, войны и побочный ущерб C2.0, замена планет колониями с сохранением хозяина, дипломатическое влияние, бонус периода от структур и деградация при их отсутствии. Ускоренная модель проходит все стадии C0.2 → C4.4 → Ct. Проверки контролируют сохранение населения, целочисленность этик, валидацию сохранения и детерминированное продолжение после сериализации.
|
||||||
|
|
||||||
|
## Профиль цивилизаций на шестой скорости
|
||||||
|
|
||||||
|
```sh
|
||||||
|
godot --headless --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- frames=20 profile
|
||||||
|
godot --path Owniverse.GI --script res://tests/benchmark_civilizations.gd -- ui scenario=mixed frames=10 profile
|
||||||
|
```
|
||||||
|
|
||||||
|
Сценарии `mixed`, `large`, `fragmented` заполняют все стадии и этики, синхронизируя
|
||||||
|
переходы; `large` использует по 6e20 цивилизаций, `fragmented` — десять партий.
|
||||||
|
Это искусственные стрессовые сценарии. `scenario=stable` использует штатные
|
||||||
|
сроки стадий. Параметр `snapshot=/tmp/civil` сохраняет состояние каждого сценария
|
||||||
|
для сравнения до и после изменения. UI запускает настоящую сцену со страницей
|
||||||
|
жизни и реальным delta, без загрузки и записи сохранения игрока.
|
||||||
|
|
||||||
|
`profile` выводит суммарные микросекунды и число вызовов по этапам. Вложенные
|
||||||
|
этапы входят во время родительских; складывать их нельзя. В игре сбор выключен.
|
||||||
|
Кэши физических количеств принадлежат отдельному миру и проверяют изменения
|
||||||
|
количеств, вложенных привязок и индексов каталога. Кэш структуры правил не входит
|
||||||
|
в сохранение: при изменении Claim/бонусов нужно перестроить индексы каталога,
|
||||||
|
при изменении home в существующей таблице — invalidate_civilization_layout().
|
||||||
|
|
||||||
|
После оптимизации на M4 Pro, по 20 шагов 1/60 секунды, среднее время:
|
||||||
|
`mixed` 295 → 235 мс, `large` 666 → 539 мс, `fragmented` 359 → 282 мс.
|
||||||
|
Полные состояния всех трёх сценариев после 20 шагов совпали до и после изменения.
|
||||||
|
Дополнительно тест распределений сравнивает новый путь с прежним на 300
|
||||||
|
случайных наборах, включая 1e40 и накопленные долги округления.
|
||||||
|
|
||||||
|
## Расчёт между кадрами
|
||||||
|
|
||||||
|
`godot --headless --path Owniverse.GI --script res://tests/test_frame_evolution.gd`
|
||||||
|
|
||||||
|
Stage вызывает `evolute_frame`: транзакция получает бюджет 40 мс и уступает
|
||||||
|
следующему кадру внутри создания партий и обработки стадий. Расчёт остаётся
|
||||||
|
на главном потоке, состояние публикуется только после полного завершения.
|
||||||
|
Длительность одной транзакции ограничена обычным шагом 1/60 секунды выбранной
|
||||||
|
скорости. Запрошенное время хранится отдельно, поэтому отставание не превращается
|
||||||
|
в один всё более тяжёлый шаг. При предельной нагрузке время симуляции может
|
||||||
|
отставать от запрошенного; это не ограничение FPS интерфейса и не удаление событий.
|
||||||
|
|
||||||
|
Производство во время транзакции выполняется после её завершения. Запрошенное
|
||||||
|
время, активный интервал, скорость и очередь производства входят в сохранение;
|
||||||
|
при загрузке незавершённый интервал пересчитывается с опубликованного состояния.
|
||||||
|
Сброс мира отменяет отложенный расчёт. Тест проверяет точное совпадение непрерывного
|
||||||
|
и распределённого расчёта, очереди, переключение скорости, восстановление и отмену.
|
||||||
|
|
||||||
|
На M4 Pro, 180 кадров реального UI на шестой скорости: смешанный сценарий
|
||||||
|
15.9 FPS (максимальный кадр 98 мс), большие количества 17.5 FPS (106 мс),
|
||||||
|
много партий 16.3 FPS (97 мс).
|
||||||
|
Это FPS отрисовки, а не число завершённых транзакций симуляции в секунду.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
|
||||||
|
func run():
|
||||||
|
var ui: bool = "ui" in OS.get_cmdline_user_args()
|
||||||
|
var scenarios: Array = ["mixed", "large", "fragmented"]
|
||||||
|
var frames: int = 120
|
||||||
|
var step_delta: float = 1.0 / 60.0
|
||||||
|
for argument in OS.get_cmdline_user_args():
|
||||||
|
if argument.begins_with("scenario="): scenarios = [argument.trim_prefix("scenario=")]
|
||||||
|
if argument.begins_with("delta="): step_delta = float(argument.trim_prefix("delta="))
|
||||||
|
if argument.begins_with("frames="): frames = maxi(2, int(argument.trim_prefix("frames=")))
|
||||||
|
Engine.max_fps = 60
|
||||||
|
if ui: DisplayServer.window_set_size(Vector2i(540, 960))
|
||||||
|
for scenario in scenarios:
|
||||||
|
var scene
|
||||||
|
var stage
|
||||||
|
var world
|
||||||
|
if ui:
|
||||||
|
scene = load("res://world/world.tscn").instantiate()
|
||||||
|
stage = scene.get_node("Stage")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||||
|
root.add_child(scene)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
world = stage.owniverse
|
||||||
|
else:
|
||||||
|
world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
Fixture.seed(world, root.get_node("Global"), scenario == "fragmented", scenario == "large", scenario == "stable")
|
||||||
|
world.profile_enabled = "profile" in OS.get_cmdline_user_args()
|
||||||
|
var samples: Array = []
|
||||||
|
if ui:
|
||||||
|
stage.stage_page_scroller.scroll_horizontal = 4 * stage.PAGE_WIDTH
|
||||||
|
for item in stage.owniverse_items.values(): item.visible = true
|
||||||
|
for page in stage.item_pages: page.visible = true
|
||||||
|
stage._set_item_description(stage.owniverse_items["C2.0"])
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
for warmup in 10: await process_frame
|
||||||
|
stage.measuring = true
|
||||||
|
for frame in frames:
|
||||||
|
await process_frame
|
||||||
|
if (frame + 1) % 10 == 0:
|
||||||
|
print("%s: measured %d UI frames" % [scenario, frame + 1])
|
||||||
|
stage.measuring = false
|
||||||
|
samples = stage.process_samples
|
||||||
|
report(scenario + " UI frame", stage.frame_samples)
|
||||||
|
else:
|
||||||
|
for frame in frames:
|
||||||
|
var started: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(step_delta)
|
||||||
|
samples.append((Time.get_ticks_usec() - started) / 1000.0)
|
||||||
|
report(scenario + (" UI handler" if ui else " CPU"), samples)
|
||||||
|
if ui:
|
||||||
|
print("Completed simulation transactions: ", world.frame_commits, "; published year: ", world.state.time.year, "; requested year: ", world.frame_clock.get("year", 0))
|
||||||
|
print("Statistics ", world.state.civilization_stats)
|
||||||
|
if world.profile_enabled: print("Profile microseconds/calls: ", world.profile)
|
||||||
|
for argument in OS.get_cmdline_user_args():
|
||||||
|
if argument.begins_with("snapshot="):
|
||||||
|
var file = FileAccess.open(argument.trim_prefix("snapshot=") + "-" + scenario + ".json", FileAccess.WRITE)
|
||||||
|
file.store_string(JSON.stringify(world.E.encode(world.state)))
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
if ui: scene.free()
|
||||||
|
quit()
|
||||||
|
|
||||||
|
func report(label: String, samples: Array):
|
||||||
|
var total: float = 0.0
|
||||||
|
for sample in samples: total += sample
|
||||||
|
samples.sort()
|
||||||
|
print("%s: mean %.2f ms, p95 %.2f ms, max %.2f ms, %.1f per second" % [label, total / samples.size(), samples[ceili(samples.size() * 0.95) - 1], samples.back(), samples.size() * 1000 / total])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b5w4sp6eepypa
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## Read-only benchmark. Loads a diagnostic JSON, never invokes SaveManager.
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
func _initialize():
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func restore_json_integers(value):
|
||||||
|
# JSON parses ordinary integer tokens as float. Large integers are $exact.
|
||||||
|
if value is float and value == floorf(value) and absf(value) <= 9007199254740991:
|
||||||
|
return int(value)
|
||||||
|
if value is Dictionary:
|
||||||
|
for key in value: value[key] = restore_json_integers(value[key])
|
||||||
|
elif value is Array:
|
||||||
|
for i in value.size(): value[i] = restore_json_integers(value[i])
|
||||||
|
return value
|
||||||
|
|
||||||
|
func run():
|
||||||
|
var args = OS.get_cmdline_user_args()
|
||||||
|
if args.is_empty():
|
||||||
|
print("Usage: --script tests/benchmark_saved_state.gd -- /path/to/save.adventure.json [delta_seconds]")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var data = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
|
||||||
|
if not data is Dictionary or data.get("format_version") != 5:
|
||||||
|
push_error("Expected diagnostic JSON format 5")
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
var initial: Dictionary = E.decode(restore_json_integers(data)).state
|
||||||
|
var delta: float = args[1].to_float() if args.size() > 1 else 1.0 / 60.0
|
||||||
|
if not is_finite(delta) or delta <= 0:
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
for speed in [1, 4, 6]:
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
world.state = initial.duplicate(true)
|
||||||
|
world.speed = speed
|
||||||
|
var elapsed: Array = []
|
||||||
|
for cycle in 6:
|
||||||
|
var start: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(delta)
|
||||||
|
elapsed.append((Time.get_ticks_usec() - start) / 1000.0)
|
||||||
|
print("Speed %d, six consecutive %s-second steps, ms: %s" % [speed, delta, elapsed])
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dji1apht4pn5y
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
|
||||||
|
func run():
|
||||||
|
for scenario in ["ordinary", "hypergiants"]:
|
||||||
|
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.speed = 6
|
||||||
|
world.state.amount.S = E.of("1e24")
|
||||||
|
world.state.amount.H = E.of("1e24")
|
||||||
|
for entry in [["RD", 1234567], ["YD", 765432], ["STN", 654321]]:
|
||||||
|
world.state.amount[entry[0]] = entry[1]
|
||||||
|
world.batches.append_batch(world.state, entry[0], entry[1], 0, 0)
|
||||||
|
if scenario == "hypergiants":
|
||||||
|
world.state.amount.HG = E.of("1e20")
|
||||||
|
world.batches.append_batch(world.state, "HG", E.of("1e20"), 0, 0)
|
||||||
|
var start_year = E.subtract(world.catalog.entities.HG.lifespan, 1000)
|
||||||
|
world.state.time = {"datetime": start_year, "year": start_year}
|
||||||
|
var samples: Array = []
|
||||||
|
var total: int = 0
|
||||||
|
for frame in 600:
|
||||||
|
var started: int = Time.get_ticks_usec()
|
||||||
|
world.evolute(1.0 / 60.0)
|
||||||
|
var duration: int = Time.get_ticks_usec() - started
|
||||||
|
samples.append(duration)
|
||||||
|
total += duration
|
||||||
|
samples.sort()
|
||||||
|
print("Speed 6 %s: 600 frames, mean %.3f ms, p95 %.3f ms, max %.3f ms, %.1f cycles/sec" % [scenario, total / 600000.0, samples[569] / 1000.0, samples.back() / 1000.0, 600000000.0 / total])
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bboo6op14es24
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
|
||||||
|
func report(label: String, samples: Array):
|
||||||
|
var total: float = 0.0
|
||||||
|
var slow: int = 0
|
||||||
|
for sample in samples:
|
||||||
|
total += sample
|
||||||
|
if sample > 33.333: slow += 1
|
||||||
|
samples.sort()
|
||||||
|
print("%s: %d samples, mean %.3f ms, p95 %.3f ms, max %.3f ms, >33ms %d" % [label, samples.size(), total / samples.size(), samples[int(samples.size() * 0.95) - 1], samples.back(), slow])
|
||||||
|
|
||||||
|
func run():
|
||||||
|
Engine.max_fps = 60
|
||||||
|
DisplayServer.window_set_size(Vector2i(540, 960))
|
||||||
|
print("Renderer: ", RenderingServer.get_current_rendering_method(), " adapter: ", RenderingServer.get_video_adapter_name())
|
||||||
|
for scenario in ["ordinary", "hypergiants"]:
|
||||||
|
for page in ([2] if "stars" in OS.get_cmdline_user_args() else [1, 2]):
|
||||||
|
var scene = load("res://world/world.tscn").instantiate()
|
||||||
|
var stage = scene.get_node("Stage")
|
||||||
|
stage.owniverse.free()
|
||||||
|
stage.set_script(load("res://tests/fixtures/benchmark_ui_stage.gd"))
|
||||||
|
root.add_child(scene)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var world = stage.owniverse
|
||||||
|
world.speed = 6
|
||||||
|
world.state.amount.S = E.of("1e24")
|
||||||
|
world.state.amount.H = E.of("1e24")
|
||||||
|
for entry in [["RD", 1234567], ["YD", 765432], ["STN", 654321]]:
|
||||||
|
world.state.amount[entry[0]] = entry[1]
|
||||||
|
world.batches.append_batch(world.state, entry[0], entry[1], 0, 0)
|
||||||
|
if scenario == "hypergiants":
|
||||||
|
world.state.amount.HG = E.of("1e20")
|
||||||
|
world.batches.append_batch(world.state, "HG", E.of("1e20"), 0, 0)
|
||||||
|
var start_year = E.subtract(world.catalog.entities.HG.lifespan, 1000)
|
||||||
|
world.state.time = {"datetime": start_year, "year": start_year}
|
||||||
|
stage.stage_page_scroller.scroll_horizontal = page * stage.PAGE_WIDTH
|
||||||
|
for item in stage.owniverse_items.values(): item.visible = true
|
||||||
|
if page == 2: stage._control_page(stage.owniverse_items.HG)
|
||||||
|
stage._set_item_description(stage.owniverse_items.HG if page == 2 else stage.owniverse_items.H)
|
||||||
|
stage._refresh_universe_ui()
|
||||||
|
for warmup in 30: await process_frame
|
||||||
|
stage.measuring = true
|
||||||
|
for frame in 600: await process_frame
|
||||||
|
stage.measuring = false
|
||||||
|
var label: String = "%s page %d" % [scenario, page]
|
||||||
|
report(label + " Stage CPU", stage.process_samples)
|
||||||
|
report(label + " frame wall", stage.frame_samples)
|
||||||
|
print("UI counters: H=", stage.owniverse_items.H.description.text, " HG=", stage.owniverse_items.HG.description.text, " date=", stage.date_time.text)
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
scene.free()
|
||||||
|
quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b3gmlo2vq6kk4
|
||||||
Vendored
+19
@@ -0,0 +1,19 @@
|
|||||||
|
extends "res://stage/stage.gd"
|
||||||
|
|
||||||
|
var measuring: bool = false
|
||||||
|
var process_samples: Array = []
|
||||||
|
var frame_samples: Array = []
|
||||||
|
var previous_frame: int = 0
|
||||||
|
|
||||||
|
# Exercise the production Stage, with isolated state and no player-save I/O.
|
||||||
|
func load_data() -> bool: return false
|
||||||
|
func save_data() -> void: pass
|
||||||
|
func _notification(_what: int) -> void: pass
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not measuring: return
|
||||||
|
var started: int = Time.get_ticks_usec()
|
||||||
|
if previous_frame: frame_samples.append((started - previous_frame) / 1000.0)
|
||||||
|
previous_frame = started
|
||||||
|
super._process(delta)
|
||||||
|
process_samples.append((Time.get_ticks_usec() - started) / 1000.0)
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
uid://d1wm7nav71y3o
|
||||||
+1
File diff suppressed because one or more lines are too long
+30
@@ -0,0 +1,30 @@
|
|||||||
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
static func seed(world, global, fragmented: bool = false, large: bool = false, stable: bool = false):
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
world.state.amount[id] = 0
|
||||||
|
world.state.stats.total_amount[id] = 0
|
||||||
|
var entity = world.catalog.entities[id]
|
||||||
|
if entity.group_as != "":
|
||||||
|
world.state.amount[entity.group_as] = 0
|
||||||
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
|
for id in ["RD", "BG", "TWD", "SNS", "MGT", "BH25", "SMBH", "SCD", "GEM", "LGG", "LSC", "LGF", "LGW", "LUB"]:
|
||||||
|
world.state.amount[id] = E.of("1e48")
|
||||||
|
world.state.amount.S = E.of("1e50")
|
||||||
|
world.state.amount.H = E.of("1e48")
|
||||||
|
world.state.binds.RD = {"HP": E.of("1e30"), "RP": E.of("1e30"), "GG": E.of("1e30"), "AB": E.of("1e30")}
|
||||||
|
var count = E.of("6e20") if large else 6000
|
||||||
|
for id in global.CivilizationRules:
|
||||||
|
if E.le(global.CivilizationRules[id].span, 0): continue
|
||||||
|
world.state.amount[id] = count
|
||||||
|
world.state.ethics[id] = {}
|
||||||
|
for ethic in ["m", "l", "w", "d", "c", "b"]:
|
||||||
|
world.state.ethics[id][ethic] = E.divide(count, 6)
|
||||||
|
world.state.batches[id] = []
|
||||||
|
var parties: int = 10 if fragmented else 1
|
||||||
|
for i in parties:
|
||||||
|
var lifespan = E.of(global.CivilizationRules[id].span) if stable else 1
|
||||||
|
world.state.batches[id].append({"amount": E.divide(count, parties), "created_from": i * 100, "created_to": 1000 + i * 100, "start_year": E.add(lifespan, i * 100), "end_year": E.add(lifespan, 1000 + i * 100)})
|
||||||
|
world.ledger.refresh_groups(world.state)
|
||||||
|
world.speed = 6
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bjloakx4qdpsj
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
+59
@@ -0,0 +1,59 @@
|
|||||||
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
# Frozen apportionment before the civilization performance refactor.
|
||||||
|
|
||||||
|
static func allocate_whole(requests: Dictionary, limit: Variant) -> Dictionary:
|
||||||
|
var total: Variant = 0
|
||||||
|
for amount in requests.values():
|
||||||
|
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: Variant = 0
|
||||||
|
for id in requests:
|
||||||
|
var expected: Variant = (E.mul_div(E.floor_value(E.maximum(0, requests[id])), available, total) if E.gt(total, 0) else 0)
|
||||||
|
result[id] = E.floor_value(expected)
|
||||||
|
assigned = E.add(assigned, result[id])
|
||||||
|
order.append({"id": id, "rest": E.subtract(expected, result[id])})
|
||||||
|
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 E.ge(assigned, available):
|
||||||
|
break
|
||||||
|
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 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: Variant, weights: Dictionary) -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
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: Variant = 0
|
||||||
|
for id in weights:
|
||||||
|
expected[id] = E.add(E.mul_div(count, weights[id], sum_weights), remainders.get(E.add(E.add(key, ":"), id), 0))
|
||||||
|
positive_sum = E.add(positive_sum, E.maximum(0, expected[id]))
|
||||||
|
var order: Array = []
|
||||||
|
var assigned: Variant = 0
|
||||||
|
for id in weights:
|
||||||
|
var normalized: Variant = (E.mul_div(E.maximum(0, expected[id]), count, positive_sum) if E.gt(positive_sum, 0) else 0)
|
||||||
|
result[id] = E.floor_value(normalized)
|
||||||
|
assigned = E.add(assigned, result[id])
|
||||||
|
order.append({"id": id, "rest": E.subtract(normalized, result[id])})
|
||||||
|
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 E.ge(assigned, count): break
|
||||||
|
result[entry.id] = E.add(result[entry.id], 1)
|
||||||
|
assigned = E.add(assigned, 1)
|
||||||
|
for id in weights:
|
||||||
|
remainders[E.add(E.add(key, ":"), id)] = E.clamp_value(E.subtract(expected[id], result[id]), E.neg(1), 1)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
uid://dxsly3qpyqrjy
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
extends "res://world/owniverse/owniverse.gd"
|
extends "res://world/owniverse/owniverse.gd"
|
||||||
|
|
||||||
# Keep the old materialized-batch path as an independent comparison mode.
|
# Compare with the materialized-batch path, without overriding the coordinator.
|
||||||
func _uses_short_lived_fast_path(_id: String) -> bool:
|
func _init() -> void:
|
||||||
return false
|
stellar.direct_completions = false
|
||||||
|
|||||||
+22
-21
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
@@ -7,41 +8,41 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(value: bool, message: String) -> void:
|
func check(value: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not value:
|
if (not value):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var actions = load("res://ui/actions/actions.tscn").instantiate()
|
var actions = load("res://ui/actions/actions.tscn").instantiate()
|
||||||
root.add_child(actions)
|
root.add_child(actions)
|
||||||
for maximum in [0.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("9e27")]:
|
||||||
actions.set_actions("test", maximum)
|
actions.set_actions("test", maximum)
|
||||||
var labels: Array = []
|
var labels: Array = []
|
||||||
var values: Array = []
|
var values: Array = []
|
||||||
for i in 5:
|
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
|
var text: String = actions.action_labels[i].text
|
||||||
check(value == floorf(value) and value <= floorf(maximum), "Whole affordable request")
|
check((E.eq(value, E.floor_value(value)) and E.le(value, E.floor_value(maximum))), "Whole affordable request")
|
||||||
if value > 0.0:
|
if E.gt(value, 0):
|
||||||
check(not values.has(value) and not labels.has(text), "Unique requests and displayed labels")
|
check(((not values.has(value)) and (not labels.has(text))), "Unique requests and displayed labels")
|
||||||
values.append(value)
|
values.append(value)
|
||||||
labels.append(text)
|
labels.append(text)
|
||||||
else:
|
else:
|
||||||
check(text == "" and actions.action_labels[i].get_parent().get_node("TextureButton").disabled, "Unused button blank and disabled")
|
check((E.eq(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")
|
if E.ge(maximum, 1): check(E.eq(values.back(), E.floor_value(maximum)), "Maximum retained")
|
||||||
actions.set_actions("test", 100.0)
|
actions.set_actions("test", 100)
|
||||||
check(actions.action_values.values() == [1.0, 10.0, 100.0, 0.0, 0.0], "Exact power of ten not duplicated")
|
check(E.eq(actions.action_values.values(), [1, 10, 100, 0, 0]), "Exact power of ten not duplicated")
|
||||||
actions.set_actions("SMBH", 25.0, func(_request): return 25.0)
|
actions.set_actions("SMBH", 25, func ( _request ) : return 25)
|
||||||
check(actions.action_values.values() == [25.0, 0.0, 0.0, 0.0, 0.0], "Identical SMBH previews collapse to one button")
|
check(E.eq(actions.action_values.values(), [25, 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)
|
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
|
||||||
check(actions.action_values.values() == [10.0, 100.0, 123.0, 0.0, 0.0], "Unproducible small requests omitted")
|
check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted")
|
||||||
check(actions.action_labels[2].text == "120", "Recipe preview displayed while original request retained")
|
check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained")
|
||||||
var emitted: Array = []
|
var emitted: Array = []
|
||||||
actions.action_pressed.connect(func(_page, amount): emitted.append(amount))
|
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
|
||||||
actions._on_action_button_pressed(2)
|
actions._on_action_button_pressed(2)
|
||||||
actions._on_action_button_pressed(4)
|
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()
|
actions.queue_free()
|
||||||
print("Action values: %d checks, %d failures" % [checks, failures])
|
print(("Action values: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
+57
-38
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
@@ -8,88 +9,106 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(value: bool, message: String) -> void:
|
func check(value: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not value:
|
if (not value):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var manager = load("res://autoload/save_manager.gd").new()
|
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)
|
DirAccess.make_dir_recursive_absolute(directory)
|
||||||
manager.save_directory = directory
|
manager.save_directory = directory
|
||||||
root.add_child(manager)
|
root.add_child(manager)
|
||||||
manager.save_completed.connect(func(filename, success, error): completions.append([filename, success, error]))
|
manager.save_completed.connect(func ( filename , success , error ) : completions.append([filename, success, error]))
|
||||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
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 data: Dictionary = {"state": world.state, "ui": {"current_page": 1}}
|
||||||
|
var initial_energy: Variant = world.state.energy
|
||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
var old_plaintext: bool = global.save_plaintext_copy
|
var old_plaintext: bool = global.save_plaintext_copy
|
||||||
global.save_plaintext_copy = true
|
global.save_plaintext_copy = true
|
||||||
check(manager.request_save("snapshot", "test-password", data), "Snapshot queued")
|
check(manager.request_save("snapshot", "test-password", data), "Snapshot queued")
|
||||||
data.state.energy = 99.0
|
data.state.energy = 99
|
||||||
data.ui.current_page = 2
|
data.ui.current_page = 2
|
||||||
var frames: int = 0
|
var frames: int = 0
|
||||||
var deadline: int = Time.get_ticks_msec() + 10000
|
var deadline: int = E.add(Time.get_ticks_msec(), 10000)
|
||||||
while manager.is_saving() and Time.get_ticks_msec() < deadline:
|
while (manager.is_saving() and E.lt(Time.get_ticks_msec(), deadline)):
|
||||||
frames += 1
|
frames = E.add(frames, 1)
|
||||||
await process_frame
|
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")
|
var saved: Dictionary = manager.load_data("snapshot", "test-password")
|
||||||
check(saved.state.energy == 1e14 and saved.ui.current_page == 1, "Worker owns a deep snapshot")
|
check((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")))
|
var plain: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("snapshot.adventure.json")))
|
||||||
check(plain.state.energy == 1e14, "Diagnostic JSON uses same snapshot")
|
check(E.eq(E.decode(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.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()
|
completions.clear()
|
||||||
data.state.energy = 1.0
|
data.state.energy = 1
|
||||||
manager.request_save("latest", "test-password", data)
|
manager.request_save("latest", "test-password", data)
|
||||||
data.state.energy = 2.0
|
data.state.energy = 2
|
||||||
manager.request_save("latest", "test-password", data)
|
manager.request_save("latest", "test-password", data)
|
||||||
data.state.energy = 3.0
|
data.state.energy = 3
|
||||||
manager.request_save("latest", "test-password", data)
|
manager.request_save("latest", "test-password", data)
|
||||||
data.state.energy = 4.0
|
data.state.energy = 4
|
||||||
manager.flush_saves()
|
manager.flush_saves()
|
||||||
check(completions.size() == 2, "Repeated requests coalesce behind active write")
|
check(E.eq(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(manager.load_data("latest", "test-password").state.energy, 3), "Last queued snapshot wins")
|
||||||
|
|
||||||
|
var pending_clock: Dictionary = preload("res://world/owniverse/simulation_time.gd").advance(world.state.time, 0.01, 1)
|
||||||
|
data.simulation = {"requested_clock": pending_clock, "active_clock": pending_clock, "speed": 1, "productions": [{"id": "H", "amount": 7, "resource": true}]}
|
||||||
|
manager.request_save("checkpoint", "test-password", data)
|
||||||
|
manager.flush_saves()
|
||||||
|
check(E.eq(manager.load_data("checkpoint", "test-password").simulation, data.simulation), "Encrypted save preserves requested time and queued production")
|
||||||
|
var malformed: Dictionary = manager._snapshot("probe", "test-password", data).payload
|
||||||
|
malformed.simulation.productions[0].id = "missing-entity"
|
||||||
|
check(manager.validate_data(malformed).contains("queued production"), "Unknown queued production is rejected before resume")
|
||||||
|
malformed = manager._snapshot("probe", "test-password", data).payload
|
||||||
|
malformed.simulation.requested_clock.year = -1
|
||||||
|
check(manager.validate_data(malformed).contains("simulation clock"), "Invalid requested clock is rejected before resume")
|
||||||
|
data.erase("simulation")
|
||||||
|
|
||||||
var incompatible_path: String = directory.path_join("incompatible.adventure")
|
var incompatible_path: String = directory.path_join("incompatible.adventure")
|
||||||
var incompatible = FileAccess.open_encrypted_with_pass(incompatible_path, FileAccess.WRITE, "test-password")
|
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()
|
incompatible.close()
|
||||||
var original: PackedByteArray = FileAccess.get_file_as_bytes(incompatible_path)
|
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.store_string("keep previous backup")
|
||||||
previous_archive.close()
|
previous_archive.close()
|
||||||
manager.blocked_saves.incompatible = true
|
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((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(((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(E.eq(FileAccess.get_file_as_bytes(E.add(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(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")
|
check(manager.request_save("incompatible", "test-password", data), "New session can queue a save")
|
||||||
manager.flush_saves()
|
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.
|
# 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.request_save("latest", "test-password", data)
|
||||||
manager.flush_saves()
|
manager.flush_saves()
|
||||||
check(not completions.back()[1] and manager.last_error.contains("Invalid save"), "Worker validation error reaches main thread")
|
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(E.eq(manager.load_data("latest", "test-password").state.energy, 3), "Invalid snapshot preserves previous save")
|
||||||
manager.blocked_saves.latest = true
|
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.
|
# 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.request_save("missing/failure", "test-password", data)
|
||||||
manager.flush_saves()
|
manager.flush_saves()
|
||||||
check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue")
|
check(manager.last_error.contains("Cannot write"), "I/O error reported without hanging queue")
|
||||||
|
|
||||||
manager.request_save("shutdown", "test-password", data)
|
manager.request_save("shutdown", "test-password", data)
|
||||||
data.state.energy = 6.0
|
data.state.energy = 6
|
||||||
manager.request_save("shutdown", "test-password", data)
|
manager.request_save("shutdown", "test-password", data)
|
||||||
manager.free()
|
manager.free()
|
||||||
var reader = load("res://autoload/save_manager.gd").new()
|
var reader = load("res://autoload/save_manager.gd").new()
|
||||||
reader.save_directory = directory
|
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()
|
reader.free()
|
||||||
world.free()
|
world.free()
|
||||||
# Exercise the stage entry point too: manager tests alone do not catch a
|
# Exercise the stage entry point too: manager tests alone do not catch a
|
||||||
@@ -99,12 +118,12 @@ func run() -> void:
|
|||||||
shared_manager.save_directory = directory
|
shared_manager.save_directory = directory
|
||||||
var stage = load("res://stage/stage.gd").new()
|
var stage = load("res://stage/stage.gd").new()
|
||||||
stage.pending_page = 2
|
stage.pending_page = 2
|
||||||
stage.owniverse.state.energy = 42.0
|
stage.owniverse.state.energy = 42
|
||||||
stage.save_data()
|
stage.save_data()
|
||||||
check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk")
|
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()
|
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.owniverse.free()
|
||||||
stage.free()
|
stage.free()
|
||||||
shared_manager.save_directory = original_directory
|
shared_manager.save_directory = original_directory
|
||||||
@@ -112,5 +131,5 @@ func run() -> void:
|
|||||||
for filename in DirAccess.get_files_at(directory):
|
for filename in DirAccess.get_files_at(directory):
|
||||||
DirAccess.remove_absolute(directory.path_join(filename))
|
DirAccess.remove_absolute(directory.path_join(filename))
|
||||||
DirAccess.remove_absolute(directory)
|
DirAccess.remove_absolute(directory)
|
||||||
print("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures])
|
print(("Async save: %d checks, %d failures (validation/block/I/O errors above are intentional)" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,41 +18,41 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.state.batch_origins.LGRB = 10357034.80000000074505806
|
world.state.batch_origins.LGRB = E.of("10357034.80000000074505806")
|
||||||
var boundary: float = 41215289.79999999701976776
|
var boundary: Variant = E.of("41215289.79999999701976776")
|
||||||
for interval in [[boundary - 2.5, boundary + 2.25], [boundary, boundary + 2.0]]:
|
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.state.batches.LGRB = []
|
||||||
world._append_batch(world.state, "LGRB", 100.0, interval[0], interval[1])
|
world.batches.append_batch(world.state, "LGRB", 100, interval[0], interval[1])
|
||||||
var total: float = 0.0
|
var total: Variant = 0
|
||||||
var last: float = -INF
|
var last: Variant = E.neg(INF)
|
||||||
for batch: Dictionary in world.state.batches.LGRB:
|
for batch : Dictionary in world.state.batches.LGRB:
|
||||||
total += batch.amount
|
total = E.add(total, batch.amount)
|
||||||
check(batch.created_from > last, "Batch windows advance monotonically")
|
check(E.gt(batch.created_from, last), "Batch windows advance monotonically")
|
||||||
check(batch.created_to > batch.created_from, "Positive window duration")
|
check(E.gt(batch.created_to, batch.created_from), "Positive window duration")
|
||||||
check(batch.amount >= 0.0, "No negative batch amounts")
|
check(E.ge(batch.amount, 0), "No negative batch amounts")
|
||||||
last = batch.created_from
|
last = batch.created_from
|
||||||
check(total == 100.0, "Boundary correction preserves all objects")
|
check(E.eq(total, 100), "Boundary correction preserves all objects")
|
||||||
check(world.state.batches.LGRB.size() <= 7, "Finite number of windows")
|
check(E.le(world.state.batches.LGRB.size(), 7), "Finite number of windows")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
print("Batch boundaries: %d checks, %d failures" % [checks, failures])
|
print(("Batch boundaries: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,148 +18,148 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func seed_population(world, id: String, count: float, time: float = 1.0) -> void:
|
func seed_population(world, id: String, count: Variant, time: Variant = 1.0) -> void:
|
||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0
|
||||||
world.state.amount[id] = count
|
world.state.amount[id] = count
|
||||||
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
world.state.batches[id] = [{"amount": count, "start_year": time, "end_year": time}]
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
|
|
||||||
func sum_ethics(world, id: String) -> float:
|
func sum_ethics(world, id: String) -> Variant:
|
||||||
var result: float = 0.0
|
var result: Variant = 0
|
||||||
for n in world.state.ethics.get(id, {}).values(): result += n
|
for n in world.state.ethics.get(id, {}).values(): result = E.add(result, n)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
func invariants(world) -> void:
|
func invariants(world) -> void:
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
for id in world.state.ethics:
|
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():
|
for n in world.state.ethics[id].values():
|
||||||
if n < 0.0 or n != floorf(n): check(false, "Invalid ethic amount")
|
if (E.lt(n, 0) or E.ne(n, E.floor_value(n))): check(false, "Invalid ethic amount")
|
||||||
check(root.get_node("SaveManager").validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Civilization state validates")
|
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Civilization state validates")
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
var baseline: Dictionary = global.CivilizationRules.duplicate(true)
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
seed_population(world, "C0.2", 60000.0)
|
seed_population(world, "C0.2", 60000)
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
check(world.state.amount["C0.3"] > 0.0, "C0.2 actually progresses")
|
check(E.gt(world.state.amount["C0.3"], 0), "C0.2 actually progresses")
|
||||||
check(world.state.amount["C0.1"] > 0.0, "C0.2 actually degrades")
|
check(E.gt(world.state.amount["C0.1"], 0), "C0.2 actually degrades")
|
||||||
check(world.state.civilization_stats.get("wars", 0.0) == 0.0, "Table disables early wars")
|
check(E.eq(world.state.civilization_stats.get("wars", 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(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(world.state.stats.total_amount["C0.2"] == 0.0, "Survivors do not count as new civilizations")
|
check(E.eq(world.state.stats.total_amount["C0.2"], 0), "Survivors do not count as new civilizations")
|
||||||
check(world.state.civilization_stats.worship > 0.0, "Worship derived from surviving ethics")
|
check(E.gt(world.state.civilization_stats.worship, 0), "Worship derived from surviving ethics")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
var restored = fixture()
|
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))))
|
||||||
world._evolve_batches(world.state, 2100.0)
|
world.evolution.evolve_batches(world.state, 2100)
|
||||||
restored._evolve_batches(restored.state, 2100.0)
|
restored.evolution.evolve_batches(restored.state, 2100)
|
||||||
check(world.state == restored.state, "Save resumes deterministic ethical evolution")
|
check(E.eq(world.state, restored.state), "Save resumes deterministic ethical evolution")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1e8
|
world.state.amount.RD = 100000000
|
||||||
seed_population(world, "C2.0", 60000.0)
|
seed_population(world, "C2.0", 60000)
|
||||||
world.state.amount["C1.9"] = 10000.0
|
world.state.amount["C1.9"] = 10000
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
check(world.state.civilization_stats.wars > 0.0, "Wars execute when enabled")
|
check(E.gt(world.state.civilization_stats.wars, 0), "Wars execute when enabled")
|
||||||
check(world.state.civilization_stats.collateral > 0.0, "Wars affect previous-stage populations")
|
check(E.gt(world.state.civilization_stats.collateral, 0), "Wars affect previous-stage populations")
|
||||||
check(world.state.amount["C2.1"] > 0.0, "Winners and peaceful progress reach next stage")
|
check(E.gt(world.state.amount["C2.1"], 0), "Winners and peaceful progress reach next stage")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1
|
||||||
world.state.binds.RD = {"HP": 5.0, "GG": 5.0, "AB": 5.0, "RP": 5.0}
|
world.state.binds.RD = {"HP": 5, "GG": 5, "AB": 5, "RP": 5}
|
||||||
seed_population(world, "C1.6", 60000.0)
|
seed_population(world, "C1.6", 60000)
|
||||||
for row: Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
for row : Dictionary in global.CivilizationRules["C1.6"].ethics.values(): row.degradation = "0"
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
var after: Dictionary = world._physical_amounts(world.state)
|
var after: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
check(world.state.civilization_stats.colonies == 20.0, "Colonization limited to real targets")
|
check(E.eq(world.state.civilization_stats.colonies, 20), "Colonization limited to real targets")
|
||||||
check(after.HP + after.GG + after.AB + after.RP == 0.0, "Colonized objects consumed once")
|
check(E.eq(E.add(E.add(E.add(after.HP, after.GG), after.AB), after.RP), 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")
|
check(E.eq(world.state.binds.RD.get("C1.4", 0), 20), "Colonies retain target host and use two-stages-earlier level")
|
||||||
var totals: float = 0.0
|
var totals: Variant = 0
|
||||||
for id in after:
|
for id in after:
|
||||||
if world.entities[id].group_as == "Planet": totals += after[id]
|
if E.eq(world.catalog.entities[id].group_as, "Planet"): totals = E.add(totals, after[id])
|
||||||
check(totals == 60020.0, "Colonization replaces planets, does not create extra worlds")
|
check(E.eq(totals, 60020), "Colonization replaces planets, does not create extra worlds")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
global.CivilizationRules = baseline.duplicate(true)
|
global.CivilizationRules = baseline.duplicate(true)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1e25
|
world.state.amount.RD = E.of("1e25")
|
||||||
world.state.amount.BG = 1e25
|
world.state.amount.BG = E.of("1e25")
|
||||||
world.state.amount.TWD = 1e25
|
world.state.amount.TWD = E.of("1e25")
|
||||||
world.state.amount.SNS = 1e25
|
world.state.amount.SNS = E.of("1e25")
|
||||||
world.state.amount.MGT = 1e25
|
world.state.amount.MGT = E.of("1e25")
|
||||||
world.state.amount.BH25 = 1e25
|
world.state.amount.BH25 = E.of("1e25")
|
||||||
world.state.amount.SMBH = 1e26
|
world.state.amount.SMBH = E.of("1e26")
|
||||||
for id in world.entities:
|
for id in world.catalog.entities:
|
||||||
if world.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.
|
# Accelerated deterministic fixture traverses every enabled stage, including Ct.
|
||||||
for id in global.CivilizationRules:
|
for id in global.CivilizationRules:
|
||||||
if id == "Ct": continue
|
if E.eq(id, "Ct"): continue
|
||||||
global.CivilizationRules[id].fights = false
|
global.CivilizationRules[id].fights = false
|
||||||
global.CivilizationRules[id].colonizes = false
|
global.CivilizationRules[id].colonizes = false
|
||||||
global.CivilizationRules[id].diplomacy = false
|
global.CivilizationRules[id].diplomacy = false
|
||||||
for row: Dictionary in global.CivilizationRules[id].ethics.values():
|
for row : Dictionary in global.CivilizationRules[id].ethics.values():
|
||||||
row.progress = "1"
|
row.progress = "1"
|
||||||
row.degradation = "0"
|
row.degradation = "0"
|
||||||
world.entities[id].lifespan = 1.0
|
world.catalog.entities[id].lifespan = 1
|
||||||
world.entities[id].lifespan_delta = 1.0
|
world.catalog.entities[id].lifespan_delta = 1
|
||||||
global.CivilizationRules[id].span = 1.0
|
global.CivilizationRules[id].span = 1
|
||||||
seed_population(world, "C0.2", 6.0)
|
seed_population(world, "C0.2", 6)
|
||||||
world._evolve_batches(world.state, 1000.0)
|
world.evolution.evolve_batches(world.state, 1000)
|
||||||
check(world.state.amount.Ct == 6.0, "Complete C0.2 -> C4.4 -> Ct chain runs")
|
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")
|
check((not world.state.batches.has("Ct")), "Transcendence is terminal")
|
||||||
invariants(world)
|
invariants(world)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
global.CivilizationRules = baseline
|
global.CivilizationRules = baseline
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 10000.0
|
world.state.amount.RD = 10000
|
||||||
world.state.amount.SCS = 1.0
|
world.state.amount.SCS = 1
|
||||||
world.state.amount["C2.4"] = 1.0
|
world.state.amount["C2.4"] = 1
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
var span: float = world.state.batches["C2.4"][0].start_year
|
var span: Variant = world.state.batches["C2.4"][0].start_year
|
||||||
check(is_equal_approx(span, 20000.0 / 3.6), "Structure bonus shortens newly registered lifespan")
|
check(E.near(span, E.divide(20000, E.of("3.6"))), "Structure bonus shortens newly registered lifespan")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
var evolution = load("res://world/owniverse/civilization_evolution.gd")
|
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 neutral: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics)
|
||||||
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
|
var diplomatic: Dictionary = evolution.drift({}, "test", balanced, global.CivilizationRules["C2.0"].ethics, true)
|
||||||
check(neutral == balanced, "Ordinary drift preserves balanced ethics")
|
check(E.eq(neutral, balanced), "Ordinary drift preserves balanced ethics")
|
||||||
check(diplomatic.d > diplomatic.w, "Diplomacy uses per-ethic influence")
|
check(E.gt(diplomatic.d, diplomatic.w), "Diplomacy uses per-ethic influence")
|
||||||
check(evolution.total(diplomatic) == 60000.0, "Diplomacy conserves population")
|
check(E.eq(evolution.total(diplomatic), 60000), "Diplomacy conserves population")
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1e15
|
world.state.amount.RD = E.of("1e15")
|
||||||
world.state.amount.BG = 1e15
|
world.state.amount.BG = E.of("1e15")
|
||||||
world.state.amount["C3.0"] = 6.0
|
world.state.amount["C3.0"] = 6
|
||||||
world.initialize_life_batches()
|
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)
|
invariants(world)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
print("Civilization evolution: %d checks, %d failures" % [checks, failures])
|
print(("Civilization evolution: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,118 +18,118 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
var global: Node = root.get_node("Global")
|
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():
|
for row : Dictionary in global.CivilizationRules.values():
|
||||||
check(row.ethics.size() == 6 or row.span == 0.0, "Every ethical stage has six rules")
|
check((E.eq(row.ethics.size(), 6) or E.eq(row.span, 0)), "Every ethical stage has six rules")
|
||||||
world.state.amount.RD = 12.0
|
world.state.amount.RD = 12
|
||||||
world.state.amount["C0.2"] = 8.0
|
world.state.amount["C0.2"] = 8
|
||||||
world.state.binds.RD = {"C2.0": 12.0, "C1.9": 3.0}
|
world.state.binds.RD = {"C2.0": 12, "C1.9": 3}
|
||||||
world.initialize_life_batches()
|
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((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(world.state.binds.RD["C1.9"] == 3.0, "Planetary civilizations retain their host")
|
check(E.eq(world.state.binds.RD["C1.9"], 3), "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(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 n == 2.0), "Bound civilization ethics count physical population")
|
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)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == before, "Repeated initialization preserves ethics")
|
check(E.eq(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}
|
world.state.ethics["C0.2"] = {"m": 8, "l": 0, "w": 0, "d": 0, "c": 0, "b": 0}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.ethics["C0.2"].m == 8.0, "Existing unequal distribution is retained")
|
check(E.eq(world.state.ethics["C0.2"].m, 8), "Existing unequal distribution is retained")
|
||||||
world.state.amount["C0.2"] = 3.0
|
world.state.amount["C0.2"] = 3
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.ethics["C0.2"].m == 3.0, "Losses reduce ethics without inventing population")
|
check(E.eq(world.state.ethics["C0.2"].m, 3), "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(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Ethics state validates")
|
||||||
var restored = fixture()
|
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.initialize_life_batches()
|
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(restored)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 10.0
|
world.state.amount.RD = 10
|
||||||
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||||
row.progress = "1"
|
row.progress = "1"
|
||||||
row.degradation = "0"
|
row.degradation = "0"
|
||||||
row.colonization = "0"
|
row.colonization = "0"
|
||||||
world.state.binds.RD = {"C1.9": 10.0}
|
world.state.binds.RD = {"C1.9": 10}
|
||||||
world.state.batches["C1.9"] = [{"amount": 10.0, "start_year": 1.0, "end_year": 1.0}]
|
world.state.batches["C1.9"] = [{"amount": 10, "start_year": 1, "end_year": 1}]
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
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")
|
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)
|
dispose(world)
|
||||||
var capacity = load("res://world/owniverse/civilization_state.gd")
|
var capacity = load("res://world/owniverse/civilization_state.gd")
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 10.0
|
world.state.amount.RD = 10
|
||||||
world.state.amount["C2.0"] = 8.0
|
world.state.amount["C2.0"] = 8
|
||||||
var budget: Dictionary = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
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(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C1.9", "C2.0", 5), 2), "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")
|
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.0
|
world.state.amount["C2.0"] = 1
|
||||||
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
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(E.eq(capacity.admit_transition(budget, global.CivilizationRules, "C2.0", "C2.1", 1), 1), "A transition reuses the source reservation")
|
||||||
check(budget.free.DwStar == 0.0, "C2.1 reserves ten dwarf stars")
|
check(E.eq(budget.free.DwStar, 0), "C2.1 reserves ten dwarf stars")
|
||||||
world.state.amount["C2.0"] = 0.0
|
world.state.amount["C2.0"] = 0
|
||||||
world.state.binds.SC = {"RD": 10.0}
|
world.state.binds.SC = {"RD": 10}
|
||||||
budget = capacity.stellar_budget(world, world.state, global.CivilizationRules)
|
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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 12.0
|
world.state.amount.RD = 12
|
||||||
world.state.amount["C2.1"] = 2.0
|
world.state.amount["C2.1"] = 2
|
||||||
world.state.amount["C2.0"] = 5.0
|
world.state.amount["C2.0"] = 5
|
||||||
world.initialize_life_batches()
|
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((E.eq(world.state.amount["C2.1"], 1) and E.eq(world.state.amount["C2.0"], 2)), "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.binds.RD["C1.9"], 4), "Excess cascades to supported planetary stage")
|
||||||
var snapshot: Dictionary = world.state.duplicate(true)
|
var snapshot: Dictionary = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == snapshot, "Capacity enforcement is idempotent")
|
check(E.eq(world.state, snapshot), "Capacity enforcement is idempotent")
|
||||||
world.state.amount.RD = 0.0
|
world.state.amount.RD = 0
|
||||||
world.initialize_life_batches()
|
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((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(world._physical_amounts(world.state)["C1.9"] == 7.0, "Forced degradation conserves civilizations")
|
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 7), "Forced degradation conserves civilizations")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.LGW = 20.0
|
world.state.amount.LGW = 20
|
||||||
world.state.amount.SMBH = 2.0e22
|
world.state.amount.SMBH = E.of("2.0e22")
|
||||||
world.state.amount["C4.2"] = 3.0
|
world.state.amount["C4.2"] = 3
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.amount["C4.2"] == 2.0, "SMBH capacity uses stellar mass equivalent")
|
check(E.eq(world.state.amount["C4.2"], 2), "SMBH capacity uses stellar mass equivalent")
|
||||||
check(world._physical_amounts(world.state)["C1.9"] == 1.0, "Unsupported civilization degrades through missing habitat families")
|
check(E.eq(world.ledger.physical_amounts(world.state)["C1.9"], 1), "Unsupported civilization degrades through missing habitat families")
|
||||||
check(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(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Capacity-limited state validates")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 2.0
|
world.state.amount.RD = 2
|
||||||
for row: Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
for row : Dictionary in global.CivilizationRules["C1.9"].ethics.values():
|
||||||
row.progress = "1"
|
row.progress = "1"
|
||||||
row.degradation = "0"
|
row.degradation = "0"
|
||||||
row.colonization = "0"
|
row.colonization = "0"
|
||||||
world.state.binds.RD = {"C1.9": 5.0}
|
world.state.binds.RD = {"C1.9": 5}
|
||||||
world.state.batches["C1.9"] = [{"amount": 5.0, "start_year": 1.0, "end_year": 1.0}]
|
world.state.batches["C1.9"] = [{"amount": 5, "start_year": 1, "end_year": 1}]
|
||||||
world._evolve_batches(world.state, 1.0)
|
world.evolution.evolve_batches(world.state, 1)
|
||||||
check(world.state.amount["C2.0"] == 2.0 and world.state.binds.RD["C1.9"] == 3.0, "Unfunded transition retains the original planetary civilization")
|
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)
|
dispose(world)
|
||||||
print("Civilization state: %d checks, %d failures" % [checks, failures])
|
print(("Civilization state: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
+44
-43
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,21 +18,21 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
@@ -39,39 +40,39 @@ func run() -> void:
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
var description = load("res://world/owniverse/item_description.gd")
|
var description = load("res://world/owniverse/item_description.gd")
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.state.amount.H = 8.0
|
world.state.amount.H = 8
|
||||||
world.state.amount.He = 2.0
|
world.state.amount.He = 2
|
||||||
for locale in ["en", "ru"]:
|
for locale in ["en", "ru"]:
|
||||||
global.Locale = locale
|
global.Locale = locale
|
||||||
var text: String = description.render(world, global, "RD")
|
var text: String = description.render(world, global, "RD")
|
||||||
check(text.contains("[b]") and not text.contains("%value"), "Localized template resolved: " + locale)
|
check((text.contains("[b]") and (not text.contains("%value"))), E.add("Localized template resolved: ", locale))
|
||||||
check(text.contains("#40cc40"), "Affordable cost green: " + locale)
|
check(text.contains("#40cc40"), E.add("Affordable cost green: ", locale))
|
||||||
check(text.contains("4.0000E+10"), "Lifespan substituted: " + locale)
|
check(text.contains("40 000 000 000"), E.add("Lifespan substituted: ", locale))
|
||||||
text = description.render(world, global, "He")
|
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")
|
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")
|
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"}
|
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")
|
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.0
|
world.state.amount.RD = 2
|
||||||
world.state.binds.G = {"RD": 3.0, "HP": 7.0}
|
world.state.binds.G = {"RD": 3, "HP": 7}
|
||||||
var values: Dictionary = description.variables(world, global, "RD", -2.0)
|
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("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((values.amount.contains("#ff6060") and values.amount.contains("−2.0")), "Real-time decrease red")
|
||||||
values = description.variables(world, global, "HP")
|
values = description.variables(world, global, "HP")
|
||||||
check(values.amount == "7", "Planet description includes bound planets")
|
check(E.eq(values.amount, "7"), "Planet description includes bound planets")
|
||||||
world.state.amount.EON1 = 2.0
|
world.state.amount.EON1 = 2
|
||||||
world.state.binds.G.EON2 = 3.0
|
world.state.binds.G.EON2 = 3
|
||||||
var saved_state: Dictionary = world.state.duplicate(true)
|
var saved_state: Dictionary = world.state.duplicate(true)
|
||||||
var counters: Dictionary = world.get_item_amounts()
|
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(((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(counters.RD == 2.0, "Star counters retain available quantity")
|
check(E.eq(counters.RD, 2), "Star counters retain available quantity")
|
||||||
check(world.state == saved_state, "Reading counters does not mutate simulation")
|
check(E.eq(world.state, saved_state), "Reading counters does not mutate simulation")
|
||||||
check(world.get_item_amounts(true).HP == 0.0, "Produced counters remain independent of current population")
|
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.0)
|
values = description.variables(world, global, "RD", 2)
|
||||||
check(values.amount.contains("#40cc40") and values.amount.contains("+2.0"), "Increase green")
|
check((values.amount.contains("#40cc40") and values.amount.contains("+2.0")), "Increase green")
|
||||||
var stage_script = load("res://stage/stage.gd")
|
var stage_script = load("res://stage/stage.gd")
|
||||||
check(stage_script.can_instantiate(), "Stage integration compiles")
|
check(stage_script.can_instantiate(), "Stage integration compiles")
|
||||||
var stage = stage_script.new()
|
var stage = stage_script.new()
|
||||||
@@ -81,9 +82,9 @@ func run() -> void:
|
|||||||
stage.g_item_descriptions = [label]
|
stage.g_item_descriptions = [label]
|
||||||
stage.description_targets[0] = {"id": "RD", "category": "Entity"}
|
stage.description_targets[0] = {"id": "RD", "category": "Entity"}
|
||||||
stage._refresh_descriptions()
|
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)
|
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)
|
stage._update_descriptions(0.25)
|
||||||
check(label.text.contains("+4.0"), "Selected description refreshes with rate per real second")
|
check(label.text.contains("+4.0"), "Selected description refreshes with rate per real second")
|
||||||
global.Locale = "en"
|
global.Locale = "en"
|
||||||
@@ -92,5 +93,5 @@ func run() -> void:
|
|||||||
label.free()
|
label.free()
|
||||||
stage.free()
|
stage.free()
|
||||||
dispose(world)
|
dispose(world)
|
||||||
print("Descriptions: %d checks, %d failures" % [checks, failures])
|
print(("Descriptions: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const B = preload("res://world/owniverse/math/big_integer.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func verify(condition: bool, label: String):
|
||||||
|
checks += 1
|
||||||
|
if not condition:
|
||||||
|
failures += 1
|
||||||
|
push_error(label)
|
||||||
|
|
||||||
|
func _initialize():
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func run():
|
||||||
|
var rng = RandomNumberGenerator.new()
|
||||||
|
rng.seed = 619
|
||||||
|
for trial in 300:
|
||||||
|
var n: int = rng.randi_range(0, 100)
|
||||||
|
var m: int = rng.randi_range(1, 100)
|
||||||
|
var a: int = rng.randi_range(0, 1000)
|
||||||
|
var b: int = rng.randi_range(0, 1000)
|
||||||
|
var expected: int = 0
|
||||||
|
for i in n: expected += (a * i + b) / m
|
||||||
|
verify(E.eq(E.floor_sum(n, a, b, m), expected), "floor sum matches explicit windows")
|
||||||
|
var huge = E.of("1e40")
|
||||||
|
verify(E.lt(E.of("1e1000"), INF), "finite huge number is below positive infinity")
|
||||||
|
verify(E.gt(E.of("-1e1000"), -INF), "finite huge negative number is above negative infinity")
|
||||||
|
verify(E.gt(INF, E.of("1e1000")), "positive infinity is above finite huge number")
|
||||||
|
verify(not E.eq(E.of("1e1000"), INF), "huge number is not infinity")
|
||||||
|
verify(E.eq(E.floor_sum(huge, 2, 1, 2), E.divide(E.multiply(huge, E.subtract(huge, 1)), 2)), "floor sum supports huge window counts")
|
||||||
|
var rows = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/big_integer_oracle.json"))
|
||||||
|
for row in rows:
|
||||||
|
var a: Array = B.parse(row[0])
|
||||||
|
var b: Array = B.parse(row[1])
|
||||||
|
var qr: Array = B.divmod(a, b)
|
||||||
|
verify(B.decimal(B.add(a, b)) == row[2], "independent big sum")
|
||||||
|
verify(B.decimal(B.multiply(a, b)) == row[3], "independent big product")
|
||||||
|
verify(B.decimal(qr[0]) == row[4], "independent big quotient")
|
||||||
|
verify(B.decimal(qr[1]) == row[5], "independent big remainder")
|
||||||
|
verify(E.fixed(E.multiply(E.of(row[0]), E.of(row[1]))) == row[3], "dynamic product")
|
||||||
|
verify(E.eq(E.mul_div(E.of(row[0]), E.of(row[1]), E.of(row[1])), E.of(row[0])), "fused conservation")
|
||||||
|
for text in ["9223372036854775807", "-9223372036854775808", "9223372036854775808", "-9223372036854775809", "1e300", "1e1000"]:
|
||||||
|
var n = E.of(text)
|
||||||
|
verify(E.eq(E.subtract(E.add(n, 1), n), 1), "unit preserved at " + text)
|
||||||
|
verify(E.eq(E.decode(JSON.parse_string(JSON.stringify(E.encode(n)))), n), "round trip " + text)
|
||||||
|
for text in ["1.25", "-1.25", "123.5"]:
|
||||||
|
verify(E.approx(E.of(text)) == float(text), "approx fraction " + text)
|
||||||
|
verify(not E.valid_encoded({"$exact": [[], "0000000000000000", 1]}), "reject empty blocks")
|
||||||
|
verify(not E.valid_encoded({"$exact": [[1, 0], "0000000000000000", 1]}), "reject noncanonical blocks")
|
||||||
|
verify(E.gt(E.power(E.of("1e400"), -1), 0) == false, "float fraction underflows beyond its range")
|
||||||
|
verify(absf(E.approx(E.logarithm(E.of("1e1000"))) - 1000 * log(10.0)) < 1e-8, "logarithm beyond float range")
|
||||||
|
verify(E.eq(E.square_root(E.of("1e1000")), E.of("1e500")), "square root beyond float range")
|
||||||
|
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.state.amount.H = E.of("1e1000")
|
||||||
|
world.state.amount.S = E.of("1e1000")
|
||||||
|
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||||
|
verify(E.finite(environment.density) and E.gt(environment.density, 0), "huge density remains finite")
|
||||||
|
verify(E.near(environment.blast, 1), "huge geometric ratio stays valid")
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
print("Dynamic math: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://8vkudj0tlnng
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
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 check_number(actual, expected: String, label: String):
|
||||||
|
var reference = E.of(expected)
|
||||||
|
checks += 1
|
||||||
|
if E.gt(E.absolute(E.subtract(actual, reference)), E.of("1e-12")):
|
||||||
|
failures += 1
|
||||||
|
push_error(label + ": " + str(actual) + " != " + expected)
|
||||||
|
|
||||||
|
func run():
|
||||||
|
# Expected strings were generated independently with Python fractions.Fraction.
|
||||||
|
var cases = JSON.parse_string(FileAccess.get_file_as_string("res://tests/fixtures/exact_oracle.json"))
|
||||||
|
for row in cases:
|
||||||
|
var a = E.of(row[0])
|
||||||
|
var b = E.of(row[1])
|
||||||
|
check_number(E.add(a, b), row[2], "sum")
|
||||||
|
check_number(E.subtract(a, b), row[3], "difference")
|
||||||
|
check_number(E.multiply(a, b), row[4], "product")
|
||||||
|
check_number(E.divide(a, b), row[5], "quotient")
|
||||||
|
check(E.floor_value(E.divide(a, b)), row[6], "floor")
|
||||||
|
for text in ["9223372036854775807", "-9223372036854775808", "9007199254740993", "1000000000000000000000000001", "-1/10000000000000"]:
|
||||||
|
var value = E.of(text)
|
||||||
|
check(E.eq(E.decode(JSON.parse_string(JSON.stringify(E.encode(value)))), value), "true", "JSON round trip")
|
||||||
|
check(E.add(E.of("9223372036854775807"), 1), "9223372036854775808", "int64 overflow promoted")
|
||||||
|
check(E.subtract(E.of("-9223372036854775808"), 1), "-9223372036854775809", "int64 underflow promoted")
|
||||||
|
check(Q.production_step([E.of("1e27")]), "1", "large integral recipe")
|
||||||
|
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_number(E.subtract(clock.datetime, E.of("1e27")), "1/1000000000", "single tick at huge age")
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
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")
|
||||||
|
check(E.bounded(count), str(count), "integer counts never quantized")
|
||||||
|
check(E.mul_div(E.of("1e27"), 15, 100), "150000000000000000000000000", "exact percentage")
|
||||||
|
check(E.mul_div(count, count, count), str(count), "fused product beyond storage range")
|
||||||
|
var limit = E.of("1e27")
|
||||||
|
check(E.floor_value(E.mul_div(E.subtract(limit, 1), E.subtract(limit, 3), E.subtract(limit, 2))), "999999999999999999999999997", "fused ratio immediately below whole boundary")
|
||||||
|
check(E.floor_value(E.divide(E.MIN_INT, -3)), "3074457345618258602", "MIN_INT division preserves quotient")
|
||||||
|
check(E.floor_value(E.divide(-10, 3)), "-4", "native negative fractional division")
|
||||||
|
check(E.mul_div(E.MAX_INT, 0, 7), "0", "native zero fused product")
|
||||||
|
check(E.subtract(E.add(count, 1), count), "1", "single unit at E27")
|
||||||
|
check(E.floor_value(E.divide(E.subtract(count, 1), count)), "0", "division must not round up a whole object")
|
||||||
|
check(E.fixed(E.add(count, E.of("0.125")), 3), "1000000000000000000000000001.125", "formatting without oversized intermediate")
|
||||||
|
check(E.scientific(E.of("9.999999e27"), 2), "1.00E+28", "display rounding at upper boundary")
|
||||||
|
check(absf(E.approx(E.power(10, -100)) / E.approx(E.of("1e-100")) - 1.0) < 1e-15, "true", "negative power retains float precision without huge intermediate")
|
||||||
|
var largest = E.of("9999999999999999999999999999")
|
||||||
|
check(E.eq(E.decode(E.encode(largest)), largest), "true", "upper range round trip")
|
||||||
|
check(E.valid_encoded(E.encode(largest)), "true", "upper range encoding valid")
|
||||||
|
for tiny in ["1e-13", "1e-20", "1e-100", "1e-310"]:
|
||||||
|
var value = E.of(tiny)
|
||||||
|
check(E.gt(value, 0), "true", "tiny fraction remains nonzero")
|
||||||
|
check(E.eq(value, E.decode(JSON.parse_string(JSON.stringify(E.encode(value))))), "true", "tiny float round trip")
|
||||||
|
for invalid in [{"$fixed": ["0", "0", "ffffffffffffffff", 1]}, {"$fixed": ["0", "-1", "0000000000000000", 1]}, {"$exact": "1/3"}, {"$exact": "10000000000000000000000000000"}, 1e100]:
|
||||||
|
check(E.valid_encoded(invalid), "false", "reject invalid saved number")
|
||||||
|
world.state.amount.S = E.of("1e24")
|
||||||
|
world.state.amount.H = E.of("1e24")
|
||||||
|
world.state.amount.RD = 1234567
|
||||||
|
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": 5, "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
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
|
const Fixture = preload("res://tests/fixtures/civilization_benchmark.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
func check(condition: bool, message: String):
|
||||||
|
checks += 1
|
||||||
|
if not condition:
|
||||||
|
failures += 1
|
||||||
|
push_error(message)
|
||||||
|
func fixture():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
Fixture.seed(world, root.get_node("Global"))
|
||||||
|
return world
|
||||||
|
func dispose(world):
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
|
||||||
|
func run():
|
||||||
|
Engine.max_fps = 0
|
||||||
|
var direct = fixture()
|
||||||
|
var sliced = fixture()
|
||||||
|
for delta in [1.0 / 60.0, 0.133333333333]:
|
||||||
|
direct.evolute(delta)
|
||||||
|
var clock: Dictionary = Clock.advance(sliced.state.time, delta, sliced.speed)
|
||||||
|
var budget = load("res://world/owniverse/frame_budget.gd").new(self, 1)
|
||||||
|
await sliced.evolution.evolute_to_datetime(clock.datetime, clock, budget)
|
||||||
|
check(E.eq(direct.state, sliced.state), "Sliced transaction exactly matches uninterrupted evolution")
|
||||||
|
dispose(direct)
|
||||||
|
dispose(sliced)
|
||||||
|
direct = fixture()
|
||||||
|
sliced = fixture()
|
||||||
|
var initial_h: Variant = sliced.state.amount.H
|
||||||
|
sliced.evolute_frame(1.0 / 60.0, self)
|
||||||
|
check(sliced.frame_busy, "Heavy transaction yields before publishing")
|
||||||
|
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
|
||||||
|
sliced.request_production("H", 1, true)
|
||||||
|
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
|
||||||
|
sliced.evolute_frame(0.01, self)
|
||||||
|
var requested: Dictionary = sliced.frame_clock.duplicate()
|
||||||
|
sliced.speed = 3
|
||||||
|
check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction")
|
||||||
|
while sliced.frame_busy: await process_frame
|
||||||
|
direct.evolute(1.0 / 60.0)
|
||||||
|
direct.production.produce_hs("H", 1)
|
||||||
|
direct.speed = 3
|
||||||
|
check(E.eq(direct.state, sliced.state), "Queued action and speed change survive publication")
|
||||||
|
sliced.evolute_frame(0.001, self)
|
||||||
|
var second_clock: Dictionary = Clock.advance(requested, 0.001, 3)
|
||||||
|
var active: Dictionary = sliced.frame_active_clock.duplicate()
|
||||||
|
while sliced.frame_busy: await process_frame
|
||||||
|
direct.evolution.evolute_to_datetime(active.datetime, active)
|
||||||
|
check(E.eq(direct.state, sliced.state), "Bounded frame transaction matches its exact target clock")
|
||||||
|
check(E.eq(sliced.frame_clock, second_clock), "All elapsed time during a busy frame remains in the requested clock")
|
||||||
|
check(sliced.frame_commits == 2, "Only complete simulation states are committed")
|
||||||
|
dispose(direct)
|
||||||
|
dispose(sliced)
|
||||||
|
var original = fixture()
|
||||||
|
original.evolute_frame(1.0 / 60.0, self)
|
||||||
|
original.request_production("H", 123, true)
|
||||||
|
original.evolute_frame(0.01, self)
|
||||||
|
original.speed = 3
|
||||||
|
var checkpoint: Dictionary = E.decode(E.encode(original.frame_checkpoint()))
|
||||||
|
var published: Dictionary = E.decode(E.encode(original.state))
|
||||||
|
var expected_clock: Dictionary = original.frame_active_clock.duplicate()
|
||||||
|
dispose(original)
|
||||||
|
for frame in 3: await process_frame
|
||||||
|
var resumed = fixture()
|
||||||
|
var expected = fixture()
|
||||||
|
resumed.state = published.duplicate(true)
|
||||||
|
expected.state = published.duplicate(true)
|
||||||
|
resumed.restore_frame_checkpoint(checkpoint)
|
||||||
|
resumed.evolute_frame(0.0, self)
|
||||||
|
while resumed.frame_busy: await process_frame
|
||||||
|
expected.evolution.evolute_to_datetime(expected_clock.datetime, expected_clock)
|
||||||
|
expected.production.produce_hs("H", 123)
|
||||||
|
expected.speed = 3
|
||||||
|
check(E.eq(expected.state, resumed.state), "Checkpoint recomputes interrupted transaction and preserves queued production")
|
||||||
|
check(E.eq(resumed.frame_clock, checkpoint.requested_clock), "Checkpoint preserves all requested elapsed time")
|
||||||
|
dispose(resumed)
|
||||||
|
dispose(expected)
|
||||||
|
var cancelled = fixture()
|
||||||
|
cancelled.evolute_frame(0.133333333333, self)
|
||||||
|
check(cancelled.frame_busy, "Cancellation fixture has suspended work")
|
||||||
|
var budget_ref: RefCounted = cancelled.frame_budget
|
||||||
|
dispose(cancelled)
|
||||||
|
for frame in 3: await process_frame
|
||||||
|
check(budget_ref.cancelled, "Resetting the world cancels suspended work without publishing")
|
||||||
|
var invalid = fixture()
|
||||||
|
var before: Dictionary = invalid.state.duplicate(true)
|
||||||
|
invalid.evolute_frame(1e308, self)
|
||||||
|
invalid.evolute(1e308)
|
||||||
|
check(not invalid.frame_busy and invalid.frame_clock.is_empty() and E.eq(before, invalid.state), "Overflowing elapsed float cannot enter either clock API")
|
||||||
|
dispose(invalid)
|
||||||
|
print("Frame evolution: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://te15r63sqhxs
|
||||||
+31
-30
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,42 +18,42 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
for id in world.entities:
|
for id in world.catalog.entities:
|
||||||
var expected: Array = []
|
var expected: Array = []
|
||||||
for key in world.entities:
|
for key in world.catalog.entities:
|
||||||
if world.entities[key].group_as == id:
|
if E.eq(world.catalog.entities[key].group_as, id):
|
||||||
expected.append(key)
|
expected.append(key)
|
||||||
check(world._is_group(id) == (not expected.is_empty()), "Group classification: " + id)
|
check(E.eq(world.catalog.is_group(id), ((not expected.is_empty()))), E.add("Group classification: ", id))
|
||||||
check(world._members(id) == (expected if not expected.is_empty() else [id]), "Group members: " + 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._members("Star")
|
var members: Array = world.catalog.members("Star")
|
||||||
members.clear()
|
members.clear()
|
||||||
check(not world._members("Star").is_empty(), "Returned members are independent")
|
check((not world.catalog.members("Star").is_empty()), "Returned members are independent")
|
||||||
world.entities.N.group_as = "TestGroup"
|
world.catalog.entities.N.group_as = "TestGroup"
|
||||||
world.rebuild_group_index()
|
world.catalog.rebuild_group_index()
|
||||||
check(world._members("TestGroup") == ["N"], "Explicit definition reload")
|
check(E.eq(world.catalog.members("TestGroup"), ["N"]), "Explicit definition reload")
|
||||||
world.entities.N.free()
|
world.catalog.entities.N.free()
|
||||||
world.entities.erase("N")
|
world.catalog.entities.erase("N")
|
||||||
check(not world._is_group("TestGroup"), "Removed definition invalidates index")
|
check((not world.catalog.is_group("TestGroup")), "Removed definition invalidates index")
|
||||||
print("Group index: %d checks, %d failures" % [checks, failures])
|
print(("Group index: %d checks, %d failures" % [checks, failures]))
|
||||||
dispose(world)
|
dispose(world)
|
||||||
quit(1 if failures else 0)
|
quit((1 if failures else 0))
|
||||||
|
|||||||
+77
-76
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,107 +18,107 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0
|
||||||
world.state.amount.HP = 40000.0
|
world.state.amount.HP = 40000
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.batches.HP.size() == 1, "Old HP receives life cohort")
|
check(E.eq(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[0].start_year, 5000000), "HP uses source span")
|
||||||
var before: Dictionary = world.state.duplicate(true)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == before, "Initialization is idempotent")
|
check(E.eq(world.state, before), "Initialization is idempotent")
|
||||||
world._evolve_batches(world.state, 5e6 + 5e6 / 40.0)
|
world.evolution.evolve_batches(world.state, E.add(5000000, E.divide(5000000, 40)))
|
||||||
check(world.state.amount.EON1 == 10.0, "HP abiogenesis follows source probability")
|
check(E.eq(world.state.amount.EON1, 10), "HP abiogenesis follows source probability")
|
||||||
check(world.state.amount.RP == 100.0, "HP degradation produces RP")
|
check(E.eq(world.state.amount.RP, 100), "HP degradation produces RP")
|
||||||
check(world.state.amount.HP == 39890.0, "Unchanged planets retry, not disappear")
|
check(E.eq(world.state.amount.HP, 39890), "Unchanged planets retry, not disappear")
|
||||||
check(world.state.stats.total_amount.HP == 0.0, "Retry does not inflate creation statistics")
|
check(E.eq(world.state.stats.total_amount.HP, 0), "Retry does not inflate creation statistics")
|
||||||
check(world.state.batches.HP.size() > 0, "Survivors get next attempt")
|
check(E.gt(world.state.batches.HP.size(), 0), "Survivors get next attempt")
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
check(physical.HP + physical.EON1 + physical.RP == 40000.0, "Life conserves planets")
|
check(E.eq(E.add(E.add(physical.HP, physical.EON1), physical.RP), 40000), "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(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Life save validates")
|
||||||
var restored = fixture()
|
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))))
|
||||||
world._evolve_batches(world.state, 11e6)
|
world.evolution.evolve_batches(world.state, 11000000)
|
||||||
restored._evolve_batches(restored.state, 11e6)
|
restored.evolution.evolve_batches(restored.state, 11000000)
|
||||||
check(world.state == restored.state, "Life continuation preserves cohorts and carries")
|
check(E.eq(world.state, restored.state), "Life continuation preserves cohorts and carries")
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1
|
||||||
world.state.binds.RD = {"EON5": 40000.0}
|
world.state.binds.RD = {"EON5": 40000}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world._evolve_batches(world.state, 5e5 + 5e5 / 40.0)
|
world.evolution.evolve_batches(world.state, E.add(500000, E.divide(500000, 40)))
|
||||||
check(world.state.binds.RD["C0.0"] == 10.0, "Intelligence emerges at the same host star")
|
check(E.eq(world.state.binds.RD["C0.0"], 10), "Intelligence emerges at the same host star")
|
||||||
check(world.state.amount["C0.0"] == 0.0, "Life transition keeps planet bound")
|
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")
|
check(world.state.batches.has("C0.0"), "New C0.0 starts civilization evolution")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var progress: float = 0.0
|
var progress: Variant = 0
|
||||||
var degraded: float = 0.0
|
var degraded: Variant = 0
|
||||||
var valid_outcomes: bool = true
|
var valid_outcomes: bool = true
|
||||||
for i in 4001:
|
for i in 4001:
|
||||||
var products: Dictionary = world._life_products(world.state, world.entities.HP, 1.0)
|
var products: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1)
|
||||||
var total: float = 0.0
|
var total: Variant = 0
|
||||||
for value in products.values():
|
for value in products.values():
|
||||||
total += value
|
total = E.add(total, value)
|
||||||
valid_outcomes = valid_outcomes and value >= 0.0 and value == floorf(value)
|
valid_outcomes = ((valid_outcomes and E.ge(value, 0)) and E.eq(value, E.floor_value(value)))
|
||||||
valid_outcomes = valid_outcomes and total == 1.0
|
valid_outcomes = (valid_outcomes and E.eq(total, 1))
|
||||||
progress += products.get("EON1", 0.0)
|
progress = E.add(progress, products.get("EON1", 0))
|
||||||
degraded += products.get("RP", 0.0)
|
degraded = E.add(degraded, products.get("RP", 0))
|
||||||
check(valid_outcomes, "Every single-planet attempt has whole outcomes and conserves population")
|
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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
|
||||||
world.state.binds.RD = {"HP": 2.0}
|
world.state.binds.RD = {"HP": 2}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state.batches.HP[0].created_from == 123.4, "Old bound worlds begin life at load time")
|
check(E.eq(world.state.batches.HP[0].created_from, E.of("123.4")), "Old bound worlds begin life at load time")
|
||||||
world._destroy_planets(world.state, "RD", 1.0, 1)
|
world.stellar.destroy_planets(world.state, "RD", 1, 1)
|
||||||
world._evolve_batches(world.state, 1e7)
|
world.evolution.evolve_batches(world.state, 10000000)
|
||||||
check(world._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.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)
|
dispose(world)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.time = {"datetime": 123.4, "year": 123.4}
|
world.state.time = {"datetime": E.of("123.4"), "year": E.of("123.4")}
|
||||||
world.state.amount["C0.0"] = 40000.0
|
world.state.amount["C0.0"] = 40000
|
||||||
world.initialize_life_batches()
|
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)
|
before = world.state.duplicate(true)
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
check(world.state == before, "Life batch initialization is idempotent")
|
check(E.eq(world.state, before), "Life batch initialization is idempotent")
|
||||||
world._evolve_batches(world.state, 2173.4)
|
world.evolution.evolve_batches(world.state, E.of("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((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(world.state.amount["C0.0"] == 38300.0, "C0.0 remainder stays alive")
|
check(E.eq(world.state.amount["C0.0"], 38300), "C0.0 remainder stays alive")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1
|
||||||
world.state.binds.RD = {"C0.1": 40000.0}
|
world.state.binds.RD = {"C0.1": 40000}
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world._evolve_batches(world.state, 2050.0)
|
world.evolution.evolve_batches(world.state, 2050)
|
||||||
check(world.state.binds.RD["C0.2"] == 700.0, "C0.1 reaches ethical civilization without losing host")
|
check(E.eq(world.state.binds.RD["C0.2"], 700), "C0.1 reaches ethical civilization without losing host")
|
||||||
check(world.state.binds.RD["C0.0"] == 1000.0, "C0.1 degradation follows Unity")
|
check(E.eq(world.state.binds.RD["C0.0"], 1000), "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")
|
check(E.eq(root.get_node("SaveManager").validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Extended life save validates")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
print("Life: %d checks, %d failures" % [checks, failures])
|
print(("Life: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
+40
-22
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(value: bool, message: String) -> void:
|
func check(value: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not value:
|
if (not value):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
@@ -21,31 +22,48 @@ func run() -> void:
|
|||||||
await process_frame
|
await process_frame
|
||||||
await process_frame
|
await process_frame
|
||||||
var world = stage.owniverse
|
var world = stage.owniverse
|
||||||
world.state.binds.RD = {"EON1": 10.0, "EON2": 4.0, "C0.0": 3.0}
|
world.state.binds.RD = {"EON1": 10, "EON2": 4, "C0.0": 3}
|
||||||
world.state.amount.EON1 = 2.0
|
world.state.amount.EON1 = 2
|
||||||
world.state.stats.total_amount.EON1 = 12.0
|
world.state.stats.total_amount.EON1 = 12
|
||||||
world.state.stats.total_amount.EON2 = 4.0
|
world.state.stats.total_amount.EON2 = 4
|
||||||
world.state.stats.total_amount["C0.0"] = 3.0
|
world.state.stats.total_amount["C0.0"] = 3
|
||||||
var before: Dictionary = world.state.duplicate(true)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
stage._refresh_universe_ui()
|
stage._refresh_universe_ui()
|
||||||
var counts: Dictionary = world.get_item_amounts()
|
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(((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(counts["C0.0"] == 3.0 and counts.C0 == 3.0 and counts.Planet == 19.0, "Civilization detail does not double the planetary total")
|
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(world.get_item_amounts(true).EON1 == 12.0 and world.get_item_amounts(true).LP == 16.0, "Historical counts retain individual and aggregate values")
|
check((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(world.state == before, "Presentation does not modify simulation state")
|
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(E.eq(decision.remaining, ["EON3"]), "Only unmet requirements remain pending")
|
||||||
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
|
||||||
check(stage.owniverse_items[id].visible, "Unlock both levels: " + id)
|
check(decision.visible.has(id), E.add("Unlock projection includes detail and alias: ", id))
|
||||||
check(stage.owniverse_items[id].description.text != "", "Populate the icon counter: " + id)
|
check((E.eq(pending, pending_before) and E.eq(world.state, before)), "Unlock evaluation leaves inputs untouched")
|
||||||
check(not stage.owniverse_items.EON3.visible, "Unreached eon remains locked")
|
var snapshot: Dictionary = world.presentation.pending_amounts.duplicate()
|
||||||
|
world.presentation.deferred_updates = true
|
||||||
|
world.state.amount.EON1 = E.add(world.state.amount.EON1, 1)
|
||||||
|
world.presentation.refresh(world.state, world.catalog)
|
||||||
|
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 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, 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)
|
stage.item_activated(stage.owniverse_items.LP)
|
||||||
check(stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting LP shows its unlocked eons")
|
check(stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting LP shows its unlocked eons")
|
||||||
stage.item_activated(stage.owniverse_items.C0)
|
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")
|
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
|
world.state.binds.RD.EON2 = 0
|
||||||
stage._refresh_universe_ui()
|
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.entities.values(): entity.free()
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
scene.free()
|
scene.free()
|
||||||
print("Life icons: %d checks, %d failures" % [checks, failures])
|
print(("Life icons: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var World = load("res://world/owniverse/owniverse.gd")
|
||||||
|
var first = World.new()
|
||||||
|
var second = World.new()
|
||||||
|
var names: Array = ["batches", "ledger", "production", "automatic", "stellar", "life", "environment", "evolution"]
|
||||||
|
var references: Array = []
|
||||||
|
for name in names:
|
||||||
|
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
|
||||||
|
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(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(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))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d2xk4ldldsgo3
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,42 +18,55 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.initialize_life_batches()
|
world.evolution.initialize_life_batches()
|
||||||
world.entities.N.cost = {"H": 2.0}
|
world.catalog.entities.N.cost = {"H": 2}
|
||||||
world.entities.N.bind = {"S": 3.0}
|
world.catalog.entities.N.bind = {"S": 3}
|
||||||
world.state.amount.H = 10.0
|
world.state.amount.H = 10
|
||||||
world.state.amount.S = 0.0
|
world.state.amount.S = 0
|
||||||
world.state.auto_resources = {"H": 10.0}
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), 0), "Default maximum respects missing bindings")
|
||||||
world.state.batches.H = [{"amount": 10.0, "start_year": 1.0, "end_year": 2.0}]
|
world.state.amount.S = 30
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), 5), "Default maximum uses recipe resources without an oversized sentinel")
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N, 2), 2), "Explicit production limit is preserved")
|
||||||
|
world.state.amount.H = E.of("1e27")
|
||||||
|
world.state.amount.S = E.of("1e27")
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.N), E.floor_value(E.divide(E.of("1e27"), 3))), "Default maximum at E27 retains integer precision")
|
||||||
|
world.state.amount.BH25 = 4
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.SMBH), world.production.smbh_available()), "Default SMBH maximum uses available black-hole mass")
|
||||||
|
world.state.amount.BH25 = 0
|
||||||
|
check(E.eq(world.production.calculate_max_producible(world.catalog.entities.SMBH), 0), "Empty SMBH maximum is zero")
|
||||||
|
world.state.amount.H = 10
|
||||||
|
world.state.amount.S = 0
|
||||||
|
world.state.auto_resources = {"H": 10}
|
||||||
|
world.state.batches.H = [{"amount": 10, "start_year": 1, "end_year": 2}]
|
||||||
var before: Dictionary = world.state.duplicate(true)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
check(not world._try_production(world.state, world.entities.N, 1.0, true), "Binding failure rejects production")
|
check((not world.production.try_production(world.state, world.catalog.entities.N, 1, true)), "Binding failure rejects production")
|
||||||
check(world.state == before, "Failure preserves costs, batches and budget")
|
check(E.eq(world.state, before), "Failure preserves costs, batches and budget")
|
||||||
world.state.amount.S = 10.0
|
world.state.amount.S = 10
|
||||||
var old_batches: Array = world.state.batches.H
|
var old_batches: Array = world.state.batches.H
|
||||||
check(world._try_production(world.state, world.entities.N, 2.0, true), "Funded production succeeds")
|
check(world.production.try_production(world.state, world.catalog.entities.N, 2, true), "Funded production succeeds")
|
||||||
check(world.state.amount.H == 6.0 and world.state.amount.S == 4.0, "Exact cost and binding")
|
check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding")
|
||||||
check(world.state.auto_resources.H == 6.0, "Exact automatic budget")
|
check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget")
|
||||||
check(old_batches[0].amount == 10.0 and world.state.batches.H[0].amount == 6.0, "Consumed batches isolated")
|
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])
|
print(("Transaction: %d checks, %d failures" % [checks, failures]))
|
||||||
dispose(world)
|
dispose(world)
|
||||||
quit(1 if failures else 0)
|
quit((1 if failures else 0))
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
const Math = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
const Legacy = preload("res://tests/fixtures/legacy_quantity_math.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("run")
|
||||||
|
|
||||||
|
func check(condition: bool, message: String) -> void:
|
||||||
|
checks = E.add(checks, 1)
|
||||||
|
if (not condition):
|
||||||
|
failures = E.add(failures, 1)
|
||||||
|
push_error(message)
|
||||||
|
|
||||||
|
func run() -> void:
|
||||||
|
var rng = RandomNumberGenerator.new()
|
||||||
|
rng.seed = 65432
|
||||||
|
var old_remainders: Dictionary = {}
|
||||||
|
var new_remainders: Dictionary = {}
|
||||||
|
for sample in 300:
|
||||||
|
var requests: Dictionary = {}
|
||||||
|
var weights: Dictionary = {}
|
||||||
|
for id in ["w", "m", "b", "c", "d", "l"]:
|
||||||
|
requests[id] = E.of(str(rng.randi_range(-10, 100)) + "." + str(rng.randi_range(0, 9)))
|
||||||
|
weights[id] = rng.randi_range(0, 100)
|
||||||
|
if sample % 3 == 0:
|
||||||
|
for id in requests: requests[id] = E.multiply(requests[id], E.of("1e40"))
|
||||||
|
var limit: Variant = E.multiply(rng.randi_range(0, 600), E.of("1e40")) if sample % 3 == 0 else rng.randi_range(0, 600)
|
||||||
|
check(E.eq(Math.allocate_whole(requests, limit), Legacy.allocate_whole(requests, limit)), "Allocation matches legacy with limits, ties and big integers")
|
||||||
|
var count: Variant = limit
|
||||||
|
check(E.eq(Math.split(new_remainders, "population", count, weights), Legacy.split(old_remainders, "population", count, weights)), "Split matches legacy rounding")
|
||||||
|
check(E.eq(new_remainders, old_remainders), "Signed remainder debt matches legacy")
|
||||||
|
check_ledger_cache()
|
||||||
|
var Catalog = load("res://world/owniverse/entity_catalog.gd")
|
||||||
|
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": E.of("3.9"), "b": E.of("9.1"), "c": E.neg(2), "d": 7}
|
||||||
|
var result: Dictionary = Math.allocate_whole(requests, limit)
|
||||||
|
check(E.eq(Math.sum(result), E.minimum(limit, 19)), "Conserve bounded allocation")
|
||||||
|
for id in result:
|
||||||
|
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: Variant = 0
|
||||||
|
for i in 10:
|
||||||
|
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, "b": 0, "c": 0}
|
||||||
|
for i in 30:
|
||||||
|
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(E.eq(catalog.entities.size(), rows.size()), "Catalog loads all definitions")
|
||||||
|
for id in catalog.entities:
|
||||||
|
var entity = catalog.entities[id]
|
||||||
|
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(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))
|
||||||
|
|
||||||
|
func check_ledger_cache() -> void:
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
world.state.amount.RD = E.of("1e40")
|
||||||
|
world.state.binds.GEM = {"RD": 7}
|
||||||
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, E.add(E.of("1e40"), 7)), "Initial cached physical amount")
|
||||||
|
var returned: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
returned.RD = 0
|
||||||
|
check(E.gt(world.ledger.physical_amounts(world.state).RD, 0), "Caller cannot mutate cached physical totals")
|
||||||
|
world.state.binds.GEM.RD = 11
|
||||||
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, E.add(E.of("1e40"), 11)), "Nested binding change invalidates physical cache")
|
||||||
|
world.state.amount.RD = 2
|
||||||
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Direct count change invalidates physical cache")
|
||||||
|
var other: Dictionary = world.state.duplicate(true)
|
||||||
|
other.binds.clear()
|
||||||
|
check(E.eq(world.ledger.physical_amounts(other).RD, 2), "Alternate state has independent physical totals")
|
||||||
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Returning to original state refreshes cache")
|
||||||
|
var revision: int = world.catalog.index_revision
|
||||||
|
world.catalog.rebuild_group_index()
|
||||||
|
check(world.catalog.index_revision > revision, "Catalog rebuild invalidates cached membership")
|
||||||
|
check(E.eq(world.ledger.physical_amounts(world.state).RD, 13), "Rebuild retains correct physical totals")
|
||||||
|
var rules: Dictionary = root.get_node("Global").CivilizationRules.duplicate(true)
|
||||||
|
var original_stages: int = world.catalog.civilization_layout(rules).stages.size()
|
||||||
|
rules["C2.0"].home = ""
|
||||||
|
world.catalog.invalidate_civilization_layout()
|
||||||
|
check(world.catalog.civilization_layout(rules).stages.size() == original_stages - 1, "Explicit rules change rebuilds civilization layout")
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bvri1enpsjisk
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
|
|
||||||
@@ -6,73 +7,104 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture(legacy: bool):
|
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:
|
for id in root.get_node("Global").OwniverseEntities:
|
||||||
world.entities[id] = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
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.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if world.entities[id].group_as != "":
|
if E.ne(world.catalog.entities[id].group_as, ""):
|
||||||
world.state.amount[world.entities[id].group_as] = 0.0
|
world.state.amount[world.catalog.entities[id].group_as] = 0
|
||||||
world.state.stats.total_amount[world.entities[id].group_as] = 0.0
|
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values(): entity.free()
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func equivalent(a: Variant, b: Variant) -> bool:
|
func equivalent(a: Variant, b: Variant) -> bool:
|
||||||
if a is Dictionary and b is Dictionary:
|
if ((a is Dictionary) and (b is Dictionary)):
|
||||||
if a.size() != b.size(): return false
|
if E.ne(a.size(), b.size()): return false
|
||||||
for key in a:
|
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
|
return true
|
||||||
if a is Array and b is Array:
|
if ((a is Array) and (b is Array)):
|
||||||
if a.size() != b.size(): return false
|
if E.ne(a.size(), b.size()): return false
|
||||||
for i in a.size():
|
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
|
return true
|
||||||
if (a is float or a is int) and (b is float or b is int):
|
if E.numeric(a) and E.numeric(b):
|
||||||
return absf(float(a) - float(b)) < 1e-6
|
# Counts must match exactly. Fractional float remainders can differ
|
||||||
return a == b
|
# when the analytic integral replaces thousands of subtractions.
|
||||||
|
if not E.eq(E.floor_value(a), E.floor_value(b)): return false
|
||||||
|
return E.le(E.absolute(E.subtract(a, b)), E.of("1e-8"))
|
||||||
|
return E.eq(a, b)
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
for parent in ["LRG", "HRG", "RSG", "RHG"]:
|
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 fast = fixture(false)
|
||||||
var legacy = fixture(true)
|
var legacy = fixture(true)
|
||||||
fast.state.amount[parent] = 20000.0
|
fast.state.amount[parent] = 20000
|
||||||
fast.state.amount.RD = 100000.0
|
fast.state.amount.RD = 100000
|
||||||
fast.state.amount.HP = 50.0
|
fast.state.amount.HP = 50
|
||||||
fast.state.amount.Met = 100.0
|
fast.state.amount.Met = 100
|
||||||
fast.state.binds.RD = {"HP": 2000.0, "C0.1": 100.0}
|
fast.state.binds.RD = {"HP": 2000, "C0.1": 100}
|
||||||
fast.state.binds[parent] = {"HP": 10.0}
|
fast.state.binds[parent] = {"HP": 10}
|
||||||
fast.state.batches[parent] = [{"amount": 20000.0, "start_year": 100.3, "end_year": 3000.7}]
|
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.
|
# Existing short-lived populations test integration with new arrivals.
|
||||||
fast.state.amount.SN = 3.0
|
fast.state.amount.SN = 3
|
||||||
fast._append_batch(fast.state, "SN", 3.0, 100.5, 100.5)
|
fast.batches.append_batch(fast.state, "SN", 3, E.of("100.5"), E.of("100.5"))
|
||||||
if year == 104.75: fast.state.energy = 7.0
|
if E.eq(year, E.of("104.75")): fast.state.energy = 7
|
||||||
legacy.state = fast.state.duplicate(true)
|
legacy.state = fast.state.duplicate(true)
|
||||||
fast._evolve_batches(fast.state, year)
|
fast.evolution.evolve_batches(fast.state, year)
|
||||||
legacy._evolve_batches(legacy.state, year)
|
legacy.evolution.evolve_batches(legacy.state, year)
|
||||||
for field in fast.state:
|
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"]:
|
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(fast)
|
||||||
dispose(legacy)
|
dispose(legacy)
|
||||||
var fast = fixture(false)
|
var fast = fixture(false)
|
||||||
var pending: Dictionary = {}
|
var pending: Dictionary = {}
|
||||||
fast._append_batch(fast.state, "SN", 10000.0, 0.25, 1667.25, pending, 1667.25)
|
fast.batches.append_batch(fast.state, "SN", 10000, E.of("0.25"), E.of("1667.25"), pending, E.of("1667.25"))
|
||||||
check(fast.state.batches.SN.size() <= 2 and pending.SN.size() == 1, "Completed windows aggregate without transient batch allocation")
|
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: float = pending.SN[0].amount
|
var total: Variant = pending.SN[0].amount
|
||||||
for batch in fast.state.batches.SN: total += batch.amount
|
for batch in fast.state.batches.SN: total = E.add(total, batch.amount)
|
||||||
check(total == 10000.0, "Direct completions plus survivors conserve event count")
|
check(E.eq(total, 10000), "Direct completions plus survivors conserve event count")
|
||||||
dispose(fast)
|
dispose(fast)
|
||||||
print("Short-lived events: %d checks, %d failures" % [checks, failures])
|
# Dense completed events must not iterate a million elapsed windows.
|
||||||
quit(0 if failures == 0 else 1)
|
# An aligned, fully completed cohort produces exactly count * lifespan.
|
||||||
|
fast = fixture(false)
|
||||||
|
pending = {}
|
||||||
|
var lifespan: Variant = fast.catalog.entities.SN.lifespan
|
||||||
|
var window: Variant = E.divide(lifespan, root.get_node("Global").batch_count) if E.gt(lifespan, 10) else lifespan
|
||||||
|
var creation_end: Variant = E.multiply(window, 1000000)
|
||||||
|
var dense_count: Variant = E.of("1e20")
|
||||||
|
var integral: Variant = fast.batches.append_batch(fast.state, "SN", dense_count, 0, creation_end, pending, E.add(creation_end, lifespan))
|
||||||
|
check(E.eq(pending.SN[0].amount, dense_count), "Million-window aggregation conserves every event")
|
||||||
|
check(E.eq(integral, E.multiply(dense_count, lifespan)), "Million-window aggregation preserves lifetime integral")
|
||||||
|
check(fast.state.batches.SN.is_empty(), "Completed million-window cohort leaves no transient batches")
|
||||||
|
dispose(fast)
|
||||||
|
# Sparse explosions: thousands of time windows but very few actual events.
|
||||||
|
# Exact interval edges and partial final windows exercise the jump boundary.
|
||||||
|
for count in [1, 3, 17]:
|
||||||
|
for year in [500, 1001]:
|
||||||
|
fast = fixture(false)
|
||||||
|
var legacy = fixture(true)
|
||||||
|
fast.state.amount.RHG = count
|
||||||
|
fast.state.batches.RHG = [{"amount": count, "start_year": E.of("0.25"), "end_year": E.of("1000.25")}]
|
||||||
|
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]), "Sparse %s at %s: %s" % [count, year, field])
|
||||||
|
dispose(fast)
|
||||||
|
dispose(legacy)
|
||||||
|
print(("Short-lived events: %d checks, %d failures" % [checks, failures]))
|
||||||
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
var checks: int = 0
|
||||||
|
var failures: int = 0
|
||||||
|
func _initialize(): call_deferred("run")
|
||||||
|
func check(value: bool, message: String):
|
||||||
|
checks += 1
|
||||||
|
if not value:
|
||||||
|
failures += 1
|
||||||
|
push_error(message)
|
||||||
|
func run():
|
||||||
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
|
world.catalog.load_definitions(root.get_node("Global").OwniverseEntities)
|
||||||
|
world.catalog.entities.N.lifespan = 24
|
||||||
|
var width: Variant = E.divide(24, root.get_node("Global").batch_count)
|
||||||
|
for count in [1, 2, 7, 11, 100, E.of("1e40")]:
|
||||||
|
for interval in [[E.of("0.25"), E.of("101.75")], [0, 120], [3, 3]]:
|
||||||
|
world.state.batches = {}
|
||||||
|
world.state.batch_origins = {"N": E.of("0.125")}
|
||||||
|
var expected: Array = []
|
||||||
|
var origin: Variant = world.state.batch_origins.N
|
||||||
|
var index: Variant = E.floor_value(E.divide(E.subtract(interval[0], origin), width))
|
||||||
|
var assigned: Variant = 0
|
||||||
|
var cursor: Variant = interval[0]
|
||||||
|
while true:
|
||||||
|
var start: Variant = E.add(origin, E.multiply(index, width))
|
||||||
|
var end: Variant = E.add(origin, E.multiply(E.add(index, 1), width))
|
||||||
|
if E.le(end, cursor):
|
||||||
|
index = E.add(index, 1)
|
||||||
|
continue
|
||||||
|
var until: Variant = E.minimum(interval[1], end)
|
||||||
|
var amount: Variant = E.subtract(count, assigned)
|
||||||
|
if E.gt(interval[1], interval[0]) and E.lt(until, interval[1]):
|
||||||
|
amount = E.subtract(E.floor_value(E.mul_div(count, E.subtract(until, interval[0]), E.subtract(interval[1], interval[0]))), assigned)
|
||||||
|
if E.gt(amount, 0):
|
||||||
|
expected.append({"created_from": start, "created_to": end, "start_year": E.add(start, 24), "end_year": E.add(end, 24), "amount": amount})
|
||||||
|
assigned = E.add(assigned, amount)
|
||||||
|
if E.ge(until, interval[1]): break
|
||||||
|
cursor = end
|
||||||
|
index = E.add(index, 1)
|
||||||
|
world.batches.append_batch(world.state, "N", count, interval[0], interval[1])
|
||||||
|
var actual: Array = []
|
||||||
|
for batch in world.state.batches.N:
|
||||||
|
if E.gt(batch.amount, 0): actual.append(batch)
|
||||||
|
check(E.eq(actual, expected), "Sparse jump preserves every nonempty quota and boundary")
|
||||||
|
var total: Variant = 0
|
||||||
|
for batch in actual: total = E.add(total, batch.amount)
|
||||||
|
check(E.eq(total, count), "Sparse jump preserves complete whole population")
|
||||||
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
|
world.free()
|
||||||
|
print("Sparse batches: %d checks, %d failures" % [checks, failures])
|
||||||
|
quit(1 if failures else 0)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://gtvie7p6j62x
|
||||||
+165
-164
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
|
|
||||||
@@ -6,9 +7,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -16,247 +17,247 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
|
for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
|
||||||
check(world._evolution_products(world.state, world.entities[id], 3.0).blast == 3.0 * world.entities[id].blast, "Entity blast determines power: " + id)
|
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.entities.SN.output_and.blast = 999.0
|
world.catalog.entities.SN.output_and.blast = 999
|
||||||
world.entities.SN.blast = 4
|
world.catalog.entities.SN.blast = 4
|
||||||
check(world._evolution_products(world.state, world.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.entities.SN.blast = 0
|
world.catalog.entities.SN.blast = 0
|
||||||
check(not world._evolution_products(world.state, world.entities.SN, 2.0).has("blast"), "Zero entity blast disables external event")
|
check((not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).has("blast")), "Zero entity blast disables external event")
|
||||||
world.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50.0}
|
world.catalog.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50}
|
||||||
check(not world._evolution_products(world.state, world.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")), "Alternative output cannot restore disabled blast")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var rd = world.entities.RD
|
var rd = world.catalog.entities.RD
|
||||||
check(rd.planets.HP.chance == 0.01, "Preserve fractional planet probability")
|
check(E.eq(rd.planets.HP.chance, E.of("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((E.eq(rd.planets.GG.amount, 2) and E.eq(rd.planets.GG.chance, E.of("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._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")
|
check(rd._parse_planet_string("").is_empty(), "Empty planet definitions stay empty")
|
||||||
for id in ["HP", "EON1", "C0.9", "C1.2"]:
|
for id in ["HP", "EON1", "C0.9", "C1.2"]:
|
||||||
check(world.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"]:
|
for id in ["RP", "GG", "AB", "C1.3", "C1.4", "C1.5"]:
|
||||||
check(not world.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.
|
# Actual GI table: HRG -> SN, with 1% GRB; no random rounding.
|
||||||
var totals: Dictionary = {}
|
var totals: Dictionary = {}
|
||||||
for i in 100:
|
for i in 100:
|
||||||
var products: Dictionary = world._evolution_products(world.state, world.entities.HRG, 1.0)
|
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1)
|
||||||
for id in products:
|
for id in products:
|
||||||
totals[id] = totals.get(id, 0.0) + products[id]
|
totals[id] = E.add(totals.get(id, 0), products[id])
|
||||||
check(totals.get("SN") == 99.0 and totals.get("GRB") == 1.0, "GI rare outcomes accumulate deterministically")
|
check((E.eq(totals.get("SN"), 99) and E.eq(totals.get("GRB"), 1)), "GI rare outcomes accumulate deterministically")
|
||||||
var restored = fixture()
|
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))))
|
||||||
check(world._evolution_products(world.state, world.entities.HRG, 37.0) == restored._evolution_products(restored.state, restored.entities.HRG, 37.0), "Evolution carry survives serialization")
|
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)
|
dispose(restored)
|
||||||
|
|
||||||
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
|
# Real RD lifespan, four stars spread over the existing lifespan/40 window.
|
||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0
|
||||||
world.state.amount.RD = 4.0
|
world.state.amount.RD = 4
|
||||||
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "RD", 4, 0, 0)
|
||||||
var lifespan: float = rd.lifespan
|
var lifespan: Variant = rd.lifespan
|
||||||
var width: float = rd.lifespan_delta
|
var width: Variant = rd.lifespan_delta
|
||||||
world._evolve_batches(world.state, lifespan)
|
world.evolution.evolve_batches(world.state, lifespan)
|
||||||
check(world.state.amount.RD == 4.0, "Window start does not kill the whole batch")
|
check(E.eq(world.state.amount.RD, 4), "Window start does not kill the whole batch")
|
||||||
world._evolve_batches(world.state, lifespan + width / 4.0)
|
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 4)))
|
||||||
check(world.state.amount.RD == 3.0 and world.state.amount.TWD == 1.0, "One quarter of a batch evolves")
|
check((E.eq(world.state.amount.RD, 3) and E.eq(world.state.amount.TWD, 1)), "One quarter of a batch evolves")
|
||||||
world._evolve_batches(world.state, lifespan + width / 2.0)
|
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 2)))
|
||||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Half the batch evolves")
|
check((E.eq(world.state.amount.RD, 2) and E.eq(world.state.amount.TWD, 2)), "Half the batch evolves")
|
||||||
world._evolve_batches(world.state, lifespan + width)
|
world.evolution.evolve_batches(world.state, E.add(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, 0) and E.eq(world.state.amount.TWD, 4)), "Window end completes evolution")
|
||||||
check(world.state.batches.RD.is_empty(), "Completed batch removed")
|
check(world.state.batches.RD.is_empty(), "Completed batch removed")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
# Creation accumulates independently for each star/planet pair.
|
# Creation accumulates independently for each star/planet pair.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 100.0
|
world.state.amount.RD = 100
|
||||||
for i in 100:
|
for i in 100:
|
||||||
world._create_planets(world.state, "RD", 1.0, 0.0, 0.0, 1.0)
|
world.stellar.create_planets(world.state, "RD", 1, 0, 0, 1)
|
||||||
check(world.state.binds.RD.HP == 1.0, "100 RD at metallicity 1 produce one HP")
|
check(E.eq(world.state.binds.RD.HP, 1), "100 RD at metallicity 1 produce one HP")
|
||||||
check(world.state.binds.RD.RP == 21.0, "Rocky planet probability retains fractions")
|
check(E.eq(world.state.binds.RD.RP, 21), "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((E.eq(world.state.binds.RD.GG, 16) and E.eq(world.state.binds.RD.AB, 120)), "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")
|
check((E.eq(world.state.amount.HP, 0) and E.eq(world.state.stats.total_amount.HP, 1)), "Created planets are bound, counted once")
|
||||||
var saves: Node = root.get_node("SaveManager")
|
var saves: Node = root.get_node("SaveManager")
|
||||||
check(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": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Planet carry save validates")
|
||||||
restored = fixture()
|
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))))
|
||||||
world._create_planets(world.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
world.stellar.create_planets(world.state, "RD", 37, 0, 0, E.of("0.7"))
|
||||||
restored._create_planets(restored.state, "RD", 37.0, 0.0, 0.0, 0.7)
|
restored.stellar.create_planets(restored.state, "RD", 37, 0, 0, E.of("0.7"))
|
||||||
check(world.state == restored.state, "Planet carry continues after save")
|
check(E.eq(world.state, restored.state), "Planet carry continues after save")
|
||||||
dispose(restored)
|
dispose(restored)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
# A partially dying dwarf sterilizes HP; survivors and RP move to its heir.
|
# A partially dying dwarf sterilizes HP; survivors and RP move to its heir.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 4.0
|
world.state.amount.RD = 4
|
||||||
world.state.binds.RD = {"HP": 8.0, "RP": 4.0, "C1.2": 4.0, "C1.3": 4.0, "GG": 4.0}
|
world.state.binds.RD = {"HP": 8, "RP": 4, "C1.2": 4, "C1.3": 4, "GG": 4}
|
||||||
world._append_batch(world.state, "RD", 4.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "RD", 4, 0, 0)
|
||||||
world._evolve_batches(world.state, lifespan + width / 2.0)
|
world.evolution.evolve_batches(world.state, E.add(lifespan, E.divide(width, 2)))
|
||||||
check(world.state.amount.RD == 2.0 and world.state.amount.TWD == 2.0, "Partial star transition with planets")
|
check((E.eq(world.state.amount.RD, 2) and E.eq(world.state.amount.TWD, 2)), "Partial star transition with planets")
|
||||||
check(world.state.binds.RD.HP + world.state.binds.TWD.HP == 4.0, "Half of HP sterilized")
|
check(E.eq(E.add(world.state.binds.RD.HP, world.state.binds.TWD.HP), 4), "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((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(world.state.binds.RD.RP + world.state.binds.TWD.RP == 10.0, "Sterilized planets become RP without loss")
|
check(E.eq(E.add(world.state.binds.RD.RP, world.state.binds.TWD.RP), 10), "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")
|
check((E.eq(world.state.amount.RP, 0) and E.eq(world.state.amount["C1.3"], 0)), "Inherited planets never become free")
|
||||||
world._evolve_batches(world.state, lifespan + width)
|
world.evolution.evolve_batches(world.state, E.add(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((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(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Inherited state validates")
|
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Inherited state validates")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.WS = 2.0
|
world.state.amount.WS = 2
|
||||||
world.state.binds.WS = {"HP": 4.0, "C1.3": 4.0, "RP": 4.0, "GG": 4.0}
|
world.state.binds.WS = {"HP": 4, "C1.3": 4, "RP": 4, "GG": 4}
|
||||||
world._append_batch(world.state, "WS", 2.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "WS", 2, 0, 0)
|
||||||
world._evolve_batches(world.state, world.entities.WS.lifespan + world.entities.WS.lifespan_delta)
|
world.evolution.evolve_batches(world.state, E.add(world.catalog.entities.WS.lifespan, world.catalog.entities.WS.lifespan_delta))
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
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((((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(world.state.amount.LRG == 2.0, "Giant evolves after local destruction")
|
check(E.eq(world.state.amount.LRG, 2), "Giant evolves after local destruction")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 10.0
|
world.state.amount.RD = 10
|
||||||
world.state.binds.RD = {"HP": 10.0, "C1.2": 10.0, "C1.3": 10.0, "RP": 0.0}
|
world.state.binds.RD = {"HP": 10, "C1.2": 10, "C1.3": 10, "RP": 0}
|
||||||
world._apply_blast(world.state, 5.0, {"blast": 1.0, "density": 1.0})
|
world.stellar.apply_blast(world.state, 5, {"blast": 1, "density": 1})
|
||||||
check(world.state.binds.RD.HP == 5.0 and world.state.binds.RD["C1.2"] == 5.0, "Blast damages vulnerable worlds proportionally")
|
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(world.state.binds.RD["C1.3"] == 10.0 and world.state.binds.RD.RP == 10.0, "Blast protects C1.3 and creates RP")
|
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._apply_blast(world.state, 1e6, {"blast": 1.0, "density": 1.0})
|
world.stellar.apply_blast(world.state, 1000000, {"blast": 1, "density": 1})
|
||||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 20.0, "Overwhelming blast is capped at available planets")
|
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")
|
check((not world.state.amount.has("blast")), "Blast is not a resource")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1
|
||||||
world.state.binds.RD = {"HP": 1.0}
|
world.state.binds.RD = {"HP": 1}
|
||||||
for i in 10:
|
for i in 10:
|
||||||
world._apply_blast(world.state, 1.0, {"blast": 1.0, "density": 0.1})
|
world.stellar.apply_blast(world.state, 1, {"blast": 1, "density": E.of("0.1")})
|
||||||
check(world.state.binds.RD.HP == 0.0 and world.state.binds.RD.RP == 1.0, "Small blast effects accumulate across cycles")
|
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)
|
dispose(world)
|
||||||
|
|
||||||
# Actual supernova output: one SN gives a neutron star, resources, and an event.
|
# Actual supernova output: one SN gives a neutron star, resources, and an event.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.SN = 1.0
|
world.state.amount.SN = 1
|
||||||
world._append_batch(world.state, "SN", 1.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "SN", 1, 0, 0)
|
||||||
world._evolve_batches(world.state, 2.0)
|
world.evolution.evolve_batches(world.state, 2)
|
||||||
check(world.state.amount.SN == 0.0 and world.state.amount.LNS == 1.0, "Supernova produces a remnant")
|
check((E.eq(world.state.amount.SN, 0) and E.eq(world.state.amount.LNS, 1)), "Supernova produces a remnant")
|
||||||
check(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(((E.eq(world.state.amount.H, 8) and E.eq(world.state.amount.He, 50)) and E.eq(world.state.amount.Met, 11)), "Supernova returns table resources")
|
||||||
check(not world.state.amount.has("blast"), "Supernova dispatches blast output")
|
check((not world.state.amount.has("blast")), "Supernova dispatches blast output")
|
||||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": world.state}) == "", "Blast state validates")
|
check(E.eq(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Blast state validates")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.H = 1000.0
|
world.state.amount.H = 1000
|
||||||
world.state.amount.Met = 100.0
|
world.state.amount.Met = 100
|
||||||
check(world.produce("RD", 100.0) == 100.0, "Manual star creation succeeds")
|
check(E.eq(world.production.produce("RD", 100), 100), "Manual star creation succeeds")
|
||||||
check(world.state.binds.RD.HP >= 1.0, "Manual creation dispatches planet formation")
|
check(E.ge(world.state.binds.RD.HP, 1), "Manual creation dispatches planet formation")
|
||||||
check(world.state.amount.H == 900.0, "Planet creation does not duplicate or spend stellar fuel")
|
check(E.eq(world.state.amount.H, 900), "Planet creation does not duplicate or spend stellar fuel")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1
|
||||||
world.state.amount.H = 100.0
|
world.state.amount.H = 100
|
||||||
world.state.amount.Met = 100.0
|
world.state.amount.Met = 100
|
||||||
world.state.auto_resources.H = 100.0
|
world.state.auto_resources.H = 100
|
||||||
world.evolute(0.1)
|
world.evolute(E.of("0.1"))
|
||||||
check(world.state.amount.RD == 101.0 and world.state.amount.H == 0.0, "Automatic formation uses its resource budget")
|
check((E.eq(world.state.amount.RD, 101) and E.eq(world.state.amount.H, 0)), "Automatic formation uses its resource budget")
|
||||||
check(world.state.binds.RD.HP >= 1.0, "Automatic formation also creates planets")
|
check(E.ge(world.state.binds.RD.HP, 1), "Automatic formation also creates planets")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.N.auto = 1.0
|
world.catalog.entities.N.auto = 1
|
||||||
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
|
# 1000 newly produced automatic H reserve 1 H, enough for 10 HBD.
|
||||||
world.evolute(100.0)
|
world.evolute(100)
|
||||||
check(world.state.amount.HBD == 10.0, "Brown dwarfs form without existing ordinary stars")
|
check(E.eq(world.state.amount.HBD, 10), "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((E.eq(world.state.amount.H, 999) and E.eq(world.state.auto_resources.H, 999)), "Brown dwarfs consume exactly their reserved hydrogen")
|
||||||
check(world.get_item_amounts().BD == 10.0 and world.state.batches.HBD.size() > 0, "Brown dwarfs appear in counter and evolution batches")
|
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.entities.N.product_per_year_amount = 0.0
|
world.catalog.entities.N.product_per_year_amount = 0
|
||||||
world.evolute(100.0)
|
world.evolute(100)
|
||||||
check(world.state.amount.HBD == 10.0, "Stored automatic budget is not taxed repeatedly")
|
check(E.eq(world.state.amount.HBD, 10), "Stored automatic budget is not taxed repeatedly")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.N.auto = 1.0
|
world.catalog.entities.N.auto = 1
|
||||||
world.state.amount.RD = 1.0
|
world.state.amount.RD = 1
|
||||||
world.evolute(50.0)
|
world.evolute(50)
|
||||||
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((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(world.state.amount.H == 0.0, "Prepaid fuel is not available to ordinary stars")
|
check(E.eq(world.state.amount.H, 0), "Prepaid fuel is not available to ordinary stars")
|
||||||
var resumed = fixture()
|
var resumed = fixture()
|
||||||
resumed.entities.N.auto = 1.0
|
resumed.catalog.entities.N.auto = 1
|
||||||
resumed.state = bytes_to_var(var_to_bytes(world.state))
|
resumed.state = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||||
world.evolute(50.0)
|
world.evolute(50)
|
||||||
resumed.evolute(50.0)
|
resumed.evolute(50)
|
||||||
check(world.state.amount.HBD == 10.0, "Accumulated quota eventually creates a whole batch")
|
check(E.eq(world.state.amount.HBD, 10), "Accumulated quota eventually creates a whole batch")
|
||||||
check(world.state == resumed.state, "Brown dwarf quota survives serialization deterministically")
|
check(E.eq(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")
|
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.0, "start_year": 101.0, "end_year": 101.0}]
|
world.state.batches.HBD = [{"amount": 10, "start_year": 101, "end_year": 101}]
|
||||||
world._evolve_batches(world.state, 101.0)
|
world.evolution.evolve_batches(world.state, 101)
|
||||||
check(world.state.amount.HBD == 0.0 and world.state.amount.CBD == 10.0 and world.get_item_amounts().BD == 10.0, "Cooling preserves aggregate brown dwarf count")
|
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(resumed)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var saw_individual: bool = false
|
var saw_individual: bool = false
|
||||||
for i in range(250):
|
for i in range(250):
|
||||||
world.state.amount.H += 1.0
|
world.state.amount.H = E.add(world.state.amount.H, 1)
|
||||||
world.state.auto_resources.H = world.state.auto_resources.get("H", 0.0) + 1.0
|
world.state.auto_resources.H = E.add(world.state.auto_resources.get("H", 0), 1)
|
||||||
var before: float = world.state.amount.HBD
|
var before: Variant = world.state.amount.HBD
|
||||||
world._produce_brown_dwarfs(world.state, 1.0, float(i), float(i + 1))
|
world.automatic.produce_brown_dwarfs(world.state, 1, E.of(i), E.of(E.add(i, 1)))
|
||||||
var born: float = world.state.amount.HBD - before
|
var born: Variant = E.subtract(world.state.amount.HBD, before)
|
||||||
check(born == 0.0 or born == 1.0, "Small fuel increments produce individual dwarfs")
|
check((E.eq(born, 0) or E.eq(born, 1)), "Small fuel increments produce individual dwarfs")
|
||||||
saw_individual = saw_individual or born == 1.0
|
saw_individual = (saw_individual or E.eq(born, 1))
|
||||||
check(saw_individual, "Random rounding does not wait for ten dwarfs")
|
check(saw_individual, "Random rounding does not wait for ten dwarfs")
|
||||||
resumed = fixture()
|
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 i in range(250, 500):
|
||||||
for simulation in [world, resumed]:
|
for simulation in [world, resumed]:
|
||||||
simulation.state.amount.H += 1.0
|
simulation.state.amount.H = E.add(simulation.state.amount.H, 1)
|
||||||
simulation.state.auto_resources.H += 1.0
|
simulation.state.auto_resources.H = E.add(simulation.state.auto_resources.H, 1)
|
||||||
simulation._produce_brown_dwarfs(simulation.state, 1.0, float(i), float(i + 1))
|
simulation.automatic.produce_brown_dwarfs(simulation.state, 1, E.of(i), E.of(E.add(i, 1)))
|
||||||
check(world.state == resumed.state, "Random sequence and prepaid fuel survive saving")
|
check(E.eq(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(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(is_equal_approx(world.state.amount.HBD + world.state.auto_star_deficits.HBD, 5.0), "Signed remainder preserves the 0.1 percent allocation")
|
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: float = world.state.amount.HBD
|
var paused_count: Variant = world.state.amount.HBD
|
||||||
for i in 10: world._produce_brown_dwarfs(world.state, 0.0, 500.0, 501.0)
|
for i in 10: world.automatic.produce_brown_dwarfs(world.state, 0, 500, 501)
|
||||||
check(world.state.amount.HBD == paused_count, "No repeated random attempts without new fuel")
|
check(E.eq(world.state.amount.HBD, paused_count), "No repeated random attempts without new fuel")
|
||||||
dispose(resumed)
|
dispose(resumed)
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
|
# Two simultaneous successors share surviving planets in the same 99:1 ratio.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.HRG.destroy_level = 0
|
world.catalog.entities.HRG.destroy_level = 0
|
||||||
world.state.amount.HRG = 100.0
|
world.state.amount.HRG = 100
|
||||||
world.state.binds.HRG = {"C1.3": 100.0}
|
world.state.binds.HRG = {"C1.3": 100}
|
||||||
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 100.0}]
|
world.state.batches.HRG = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 100}]
|
||||||
world._evolve_batches(world.state, 0.1)
|
world.evolution.evolve_batches(world.state, E.of("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((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(world.state.binds.HRG["C1.3"] == 0.0, "Alternative evolution leaves no orphan bindings")
|
check(E.eq(world.state.binds.HRG["C1.3"], 0), "Alternative evolution leaves no orphan bindings")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount.N = 0.0
|
world.state.amount.N = 0
|
||||||
world.state.amount.HRG = 1.0
|
world.state.amount.HRG = 1
|
||||||
world.state.batches.HRG = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
world.state.batches.HRG = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 1}]
|
||||||
world.evolute(3.0)
|
world.evolute(3)
|
||||||
check(world.state.amount.LNS == 1.0 and world.state.amount.SN == 0.0, "Long step follows multiple stellar generations")
|
check((E.eq(world.state.amount.LNS, 1) and E.eq(world.state.amount.SN, 0)), "Long step follows multiple stellar generations")
|
||||||
check(world.state.amount.S >= 99.0, "Short-lived newborn supernova emits S before disappearing")
|
check(E.ge(world.state.amount.S, 99), "Short-lived newborn supernova emits S before disappearing")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
print("Stellar rules: %d checks, %d failures" % [checks, failures])
|
print(("Stellar rules: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
+48
-46
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
const Clock = preload("res://world/owniverse/simulation_time.gd")
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
@@ -17,75 +18,76 @@ func fixture():
|
|||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
for id in global.OwniverseEntities:
|
for id in global.OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(global.OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
world.entities["N"].life_product = {}
|
world.catalog.entities["N"].life_product = {}
|
||||||
world.entities["N"].product_per_year_type = "H"
|
world.catalog.entities["N"].product_per_year_type = "H"
|
||||||
world.entities["N"].product_per_year_amount = 10.0
|
world.catalog.entities["N"].product_per_year_amount = 10
|
||||||
world.entities["N"].auto = 0.0
|
world.catalog.entities["N"].auto = 0
|
||||||
world.state.amount.N = 1.0
|
world.state.amount.N = 1
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values():
|
for entity in world.catalog.entities.values():
|
||||||
entity.free()
|
entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var saves: Node = root.get_node("SaveManager")
|
var saves: Node = root.get_node("SaveManager")
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.evolute(0.09)
|
world.evolute(E.of("0.09"))
|
||||||
check(world.state.time.year == 0.0, "Do not process before a tenth boundary")
|
check(E.eq(world.state.time.year, 0), "Do not process before a tenth boundary")
|
||||||
check(world.state.amount.H == 0.0, "No premature production")
|
check(E.eq(world.state.amount.H, 0), "No premature production")
|
||||||
world.evolute(0.01)
|
world.evolute(E.of("0.01"))
|
||||||
check(world.state.time.year == 0.1, "Process the first tenth")
|
check(E.eq(world.state.time.year, E.of("0.1")), "Process the first tenth")
|
||||||
check(world.state.amount.H == 1.0, "Production runs at 0.1 year")
|
check(E.eq(world.state.amount.H, 1), "Production runs at 0.1 year")
|
||||||
world.evolute(0.2)
|
world.evolute(E.of("0.2"))
|
||||||
check(world.state.time.year == 0.3 and world.state.amount.H == 3.0, "Do not reprocess previous tenths")
|
check((E.eq(world.state.time.year, E.of("0.3")) and E.eq(world.state.amount.H, 3)), "Do not reprocess previous tenths")
|
||||||
var before: Dictionary = world.state.duplicate(true)
|
var before: Dictionary = world.state.duplicate(true)
|
||||||
for invalid in [0.0, -1.0, NAN, INF, 1e308]:
|
for invalid in [0, E.neg(1), NAN, INF, 1e308]:
|
||||||
world.evolute(invalid)
|
world.evolute(invalid)
|
||||||
check(world.state == before, "Invalid/overflow interval rejected")
|
check(E.eq(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(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": world.state}), ""), "Fractional save is valid")
|
||||||
var invalid_save: Dictionary = world.state.duplicate(true)
|
var invalid_save: Dictionary = world.state.duplicate(true)
|
||||||
invalid_save.time.year = 0.25
|
invalid_save.time.year = E.of("0.25")
|
||||||
check(saves.validate_data({"format_version": 2, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}) != "", "Reject inconsistent fractional clock")
|
check(E.ne(saves.validate_data({"format_version": 5, "app_version": "", "ui": {"current_page": 0}, "state": invalid_save}), ""), "Reject inconsistent fractional clock")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
for fps in [30, 60, 144]:
|
for fps in [30, 60, 144]:
|
||||||
world = fixture()
|
world = fixture()
|
||||||
for frame in fps:
|
for frame in fps:
|
||||||
world.evolute(1.0 / fps)
|
world.evolute(E.divide(1, fps))
|
||||||
check(world.state.time.year == 1.0, "One second at %d fps" % fps)
|
check(E.eq(world.state.time.year, 1), ("One second at %d fps" % fps))
|
||||||
check(world.state.amount.H == 10.0, "Production across frames at %d fps" % fps)
|
check(E.eq(world.state.amount.H, 10), ("Production across frames at %d fps" % fps))
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.evolute(0.05)
|
world.evolute(E.of("0.05"))
|
||||||
world.speed = 2
|
world.speed = 2
|
||||||
world.evolute(0.005)
|
world.evolute(E.of("0.005"))
|
||||||
check(world.state.time.year == 0.1, "Speed change preserves accumulated fraction")
|
check(E.eq(world.state.time.year, E.of("0.1")), "Speed change preserves accumulated fraction")
|
||||||
world.speed = 100
|
world.speed = 100
|
||||||
check(world.speed == 5 and world.state.speed_in_year == 10000.0, "Upper speed limit unchanged")
|
var speed_limit: int = root.get_node("Global").SPEED_LIMIT
|
||||||
world.speed = -10
|
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")
|
||||||
check(world.speed == 1 and world.state.speed_in_year == 1.0, "Lower speed limit unchanged")
|
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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.entities.N.output_and = {"He": 1.0}
|
world.catalog.entities.N.output_and = {"He": 1}
|
||||||
world.entities.N.output_or = {}
|
world.catalog.entities.N.output_or = {}
|
||||||
world.state.batches.N = [{"start_year": 0.1, "end_year": 0.1, "amount": 1.0}]
|
world.state.batches.N = [{"start_year": E.of("0.1"), "end_year": E.of("0.1"), "amount": 1}]
|
||||||
world.evolute(0.1)
|
world.evolute(E.of("0.1"))
|
||||||
check(world.state.amount.N == 0.0 and world.state.amount.He == 1.0, "Existing evolution executes at fractional boundary")
|
check((E.eq(world.state.amount.N, 0) and E.eq(world.state.amount.He, 1)), "Existing evolution executes at fractional boundary")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
check(Clock.processed_year(0.099) == 0.0, "Do not round a genuine incomplete tenth forward")
|
check(E.eq(Clock.processed_year(E.of("0.099")), 0), "Do not round a genuine incomplete tenth forward")
|
||||||
check(Clock.processed_year(1e9 + 0.45) == 1e9 + 0.4, "Large ages preserve fractional precision")
|
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])
|
print(("Time state: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
+33
-27
@@ -1,4 +1,5 @@
|
|||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
var checks: int = 0
|
var checks: int = 0
|
||||||
var failures: int = 0
|
var failures: int = 0
|
||||||
@@ -7,9 +8,9 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(value: bool, message: String) -> void:
|
func check(value: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not value:
|
if (not value):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
@@ -22,34 +23,39 @@ func run() -> void:
|
|||||||
await process_frame
|
await process_frame
|
||||||
stage.stage_page_scroller.scroll_horizontal = stage.PAGE_WIDTH
|
stage.stage_page_scroller.scroll_horizontal = stage.PAGE_WIDTH
|
||||||
await process_frame
|
await process_frame
|
||||||
stage.owniverse.state.amount.H = 1000.0
|
stage.owniverse.state.amount.H = 1000
|
||||||
stage.owniverse.state.amount.RD = 2.0
|
stage.owniverse.state.amount.RD = 2
|
||||||
stage.owniverse.refresh_item_amounts()
|
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||||
stage._update_ui(0.0)
|
stage.set_owniverse_item_value()
|
||||||
var hydrogen = stage.owniverse_items.H
|
var hydrogen = stage.owniverse_items.H
|
||||||
var dwarf = stage.owniverse_items.RD
|
var dwarf = stage.owniverse_items.RD
|
||||||
check(hydrogen.description.text == "1 000", "Visible page updates immediately on navigation")
|
check(E.eq(hydrogen.description.text, "1 000"), "Pending quantities reach controls")
|
||||||
check(dwarf.description.text == "", "Offscreen counters are not rendered")
|
check(E.eq(dwarf.description.text, "2"), "Other page receives its current quantity")
|
||||||
stage.owniverse.state.amount.H = 2000.0
|
check(stage.owniverse.presentation.pending_amounts.is_empty(), "Published queue is consumed by Stage")
|
||||||
stage.owniverse.refresh_item_amounts()
|
stage.owniverse.presentation.deferred_updates = true
|
||||||
check(stage.owniverse.item_amounts_dirty and stage.owniverse.items_to_update.H == 1000.0, "Simulation invalidates without recalculating display sums")
|
stage.owniverse.state.amount.H = 2000
|
||||||
stage._update_ui(0.05)
|
stage.owniverse.presentation.refresh(stage.owniverse.state, stage.owniverse.catalog)
|
||||||
check(hydrogen.description.text == "1 000", "No refresh before interval")
|
check((stage.owniverse.presentation.dirty and stage.owniverse.presentation.pending_amounts.is_empty()), "Deferred refresh marks dirty without republishing")
|
||||||
stage._update_ui(0.051)
|
stage.set_owniverse_item_value()
|
||||||
check(hydrogen.description.text == "2 000" and not stage.owniverse.item_amounts_dirty, "Latest state appears at interval")
|
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((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.item_activated(hydrogen)
|
||||||
check(hydrogen.description.text == root.get_node("Global").format_number(stage.owniverse.state.amount.H), "Player production updates counter immediately")
|
stage.set_owniverse_item_value()
|
||||||
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
|
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
|
await process_frame
|
||||||
stage._update_ui(0.0)
|
stage._refresh_universe_ui()
|
||||||
check(dwarf.description.text == "2", "Newly visible page receives cached latest values")
|
check(E.eq(dwarf.description.text, "2"), "Newly visible page receives cached latest values")
|
||||||
stage.owniverse.state.time.year = 1234.5
|
stage.owniverse.state.time.year = E.of("1234.5")
|
||||||
stage._update_ui(0.1)
|
stage._refresh_universe_ui()
|
||||||
check(stage.date_time.text == "1 234.5", "Time uses throttled display with tenths")
|
check(E.eq(stage.date_time.text, "1 234.5"), "Time displays tenths with thousands separator")
|
||||||
for entity in stage.owniverse.entities.values(): entity.free()
|
for entity in stage.owniverse.catalog.entities.values(): entity.free()
|
||||||
stage.owniverse.free()
|
stage.owniverse.free()
|
||||||
scene.free()
|
scene.free()
|
||||||
print("UI updates: %d checks, %d failures" % [checks, failures])
|
print(("UI updates: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
+104
-105
@@ -1,4 +1,7 @@
|
|||||||
extends SceneTree
|
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")
|
||||||
|
|
||||||
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
const Descriptions = preload("res://world/owniverse/item_description.gd")
|
const Descriptions = preload("res://world/owniverse/item_description.gd")
|
||||||
@@ -9,140 +12,140 @@ func _initialize() -> void:
|
|||||||
call_deferred("run")
|
call_deferred("run")
|
||||||
|
|
||||||
func check(condition: bool, message: String) -> void:
|
func check(condition: bool, message: String) -> void:
|
||||||
checks += 1
|
checks = E.add(checks, 1)
|
||||||
if not condition:
|
if (not condition):
|
||||||
failures += 1
|
failures = E.add(failures, 1)
|
||||||
push_error(message)
|
push_error(message)
|
||||||
|
|
||||||
func fixture():
|
func fixture():
|
||||||
var world = load("res://world/owniverse/owniverse.gd").new()
|
var world = load("res://world/owniverse/owniverse.gd").new()
|
||||||
for id in root.get_node("Global").OwniverseEntities:
|
for id in root.get_node("Global").OwniverseEntities:
|
||||||
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
|
||||||
world.entities[id] = entity
|
world.catalog.entities[id] = entity
|
||||||
world.state.amount[id] = 0.0
|
world.state.amount[id] = 0
|
||||||
world.state.stats.total_amount[id] = 0.0
|
world.state.stats.total_amount[id] = 0
|
||||||
if entity.group_as != "":
|
if E.ne(entity.group_as, ""):
|
||||||
world.state.amount[entity.group_as] = 0.0
|
world.state.amount[entity.group_as] = 0
|
||||||
world.state.stats.total_amount[entity.group_as] = 0.0
|
world.state.stats.total_amount[entity.group_as] = 0
|
||||||
return world
|
return world
|
||||||
|
|
||||||
func dispose(world) -> void:
|
func dispose(world) -> void:
|
||||||
for entity in world.entities.values(): entity.free()
|
for entity in world.catalog.entities.values(): entity.free()
|
||||||
world.free()
|
world.free()
|
||||||
|
|
||||||
func run() -> void:
|
func run() -> void:
|
||||||
var global: Node = root.get_node("Global")
|
var global: Node = root.get_node("Global")
|
||||||
var world = fixture()
|
var world = fixture()
|
||||||
world.state.amount.RD = 400.0
|
world.state.amount.RD = 400
|
||||||
world._append_batch(world.state, "RD", 400.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "RD", 400, 0, 0)
|
||||||
var end: float = world.state.batches.RD[0].end_year
|
var end: Variant = world.state.batches.RD[0].end_year
|
||||||
world.modify_world_parameter("Bur", 9.0)
|
world.environment.modify_world_parameter("Bur", 9)
|
||||||
check(is_equal_approx(world.state.batches.RD[0].end_year, end / 10.0), "Burnability accelerates existing cohorts")
|
check(E.near(world.state.batches.RD[0].end_year, E.divide(end, 10)), "Burnability accelerates existing cohorts")
|
||||||
var count: float = world.state.batches.RD[0].amount
|
var count: Variant = world.state.batches.RD[0].amount
|
||||||
world.modify_world_parameter("Bur", -9.0)
|
world.environment.modify_world_parameter("Bur", E.neg(9))
|
||||||
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")
|
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.modify_world_parameter("Bur", 9.0)
|
world.environment.modify_world_parameter("Bur", 9)
|
||||||
world._append_batch(world.state, "YD", 10.0, 0.0, 0.0)
|
world.batches.append_batch(world.state, "YD", 10, 0, 0)
|
||||||
check(is_equal_approx(world.state.batches.YD[0].start_year, world.entities.YD.lifespan / 10.0), "New stars use the same burnability")
|
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": end / 10.0 - 0.1, "year": end / 10.0 - 0.1}
|
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._withdraw_batches(world.state, "RD", world.state.time.year)
|
world.batches.withdraw_batches(world.state, "RD", world.state.time.year)
|
||||||
world.modify_world_parameter("Bur", -18.0)
|
world.environment.modify_world_parameter("Bur", E.neg(18))
|
||||||
check(world.state.batches.RD[0].end_year >= world.state.time.year, "Slowing a partially evolved cohort keeps its remaining window in the future")
|
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.modify_world_parameter("Exp", 9.0)
|
world.environment.modify_world_parameter("Exp", 9)
|
||||||
check(world._evolution_products(world.state, world.entities.SN, 2.0).blast == 180.0, "Explosion power includes world explosiveness")
|
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._evolution_products(world.state, world.entities.HRG, 1000.0)
|
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000)
|
||||||
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 900.0, "Explosiveness raises rare-event probability without duplicating stars")
|
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.entities.HRG.output_or.chance = 50.0
|
world.catalog.entities.HRG.output_or.chance = 50
|
||||||
products = world._evolution_products(world.state, world.entities.HRG, 100.0)
|
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100)
|
||||||
check(products.get("GRB", 0.0) == 100.0 and products.get("SN", 0.0) == 0.0, "Rare-event probability is bounded at one")
|
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)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var ordinary: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||||
world.state.life_remainders.clear()
|
world.state.life_remainders.clear()
|
||||||
world.modify_world_parameter("Agg", 9.0)
|
world.environment.modify_world_parameter("Agg", 9)
|
||||||
var aggressive: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||||
check(aggressive.EON1 > ordinary.EON1, "Aggression accelerates primitive evolution")
|
check(E.gt(aggressive.EON1, ordinary.EON1), "Aggression accelerates primitive evolution")
|
||||||
world.state.life_remainders.clear()
|
world.state.life_remainders.clear()
|
||||||
world.state.world_parameters.Order = 10.0
|
world.state.world_parameters.Order = 10
|
||||||
var orderly: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||||
check(orderly.RP < aggressive.RP, "Order reduces degradation")
|
check(E.lt(orderly.RP, aggressive.RP), "Order reduces degradation")
|
||||||
world.state.life_remainders.clear()
|
world.state.life_remainders.clear()
|
||||||
world.state.world_parameters.Enlightenment = 10.0
|
world.state.world_parameters.Enlightenment = 10
|
||||||
var enlightened: Dictionary = world._life_products(world.state, world.entities.HP, 1000000.0)
|
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
|
||||||
check(enlightened.EON1 > orderly.EON1, "Enlightenment increases primitive progress")
|
check(E.gt(enlightened.EON1, orderly.EON1), "Enlightenment increases primitive progress")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var hotter = fixture()
|
var hotter = fixture()
|
||||||
for simulation in [world, hotter]:
|
for simulation in [world, hotter]:
|
||||||
simulation.state.amount.RD = 1000000.0
|
simulation.state.amount.RD = 1000000
|
||||||
simulation._append_batch(simulation.state, "RD", 1000000.0, 0.0, 0.0)
|
simulation.batches.append_batch(simulation.state, "RD", 1000000, 0, 0)
|
||||||
hotter.modify_world_parameter("Bur", 9.0)
|
hotter.environment.modify_world_parameter("Bur", 9)
|
||||||
world.evolute(1000.0)
|
world.evolute(1000)
|
||||||
hotter.evolute(1000.0)
|
hotter.evolute(1000)
|
||||||
check(hotter.state.amount.S > world.state.amount.S, "Burnability changes actual space production")
|
check(E.gt(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")
|
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(world)
|
||||||
dispose(hotter)
|
dispose(hotter)
|
||||||
|
|
||||||
world = fixture()
|
world = fixture()
|
||||||
world.state.amount["C0.2"] = 1000000.0
|
world.state.amount["C0.2"] = 1000000
|
||||||
world.state.ethics["C0.2"] = {"m": 1000000.0}
|
world.state.ethics["C0.2"] = {"m": 1000000}
|
||||||
var baseline: Dictionary = world.state.duplicate(true)
|
var baseline: Dictionary = world.state.duplicate(true)
|
||||||
var advanced: Dictionary = baseline.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 next_stage: String = global.CivilizationRules["C0.2"].next
|
||||||
var outcomes: Array = []
|
var outcomes: Array = []
|
||||||
for target in [baseline, advanced]:
|
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 = world.CivilizationState.stellar_budget(world, target, global.CivilizationRules)
|
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
|
||||||
world.CivilizationEvolution.prepare(world, target, expired, 1.0, budget, global.CivilizationRules)
|
CivilizationEvolution.prepare(world, target, expired, 1, budget, global.CivilizationRules)
|
||||||
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0.0))
|
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0))
|
||||||
check(outcomes[1] > outcomes[0], "Aggression changes actual ethical civilization transitions")
|
check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions")
|
||||||
dispose(world)
|
dispose(world)
|
||||||
|
|
||||||
# Trends use ethical proportions and stage weights, not population magnitude.
|
# Trends use ethical proportions and stage weights, not population magnitude.
|
||||||
world = fixture()
|
world = fixture()
|
||||||
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
|
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 source: Dictionary = world.state.duplicate(true)
|
||||||
var single: Dictionary = source.duplicate(true)
|
var single: Dictionary = source.duplicate(true)
|
||||||
var split: Dictionary = source.duplicate(true)
|
var split: Dictionary = source.duplicate(true)
|
||||||
Parameters.advance(source, single, rules, 1.0e6)
|
Parameters.advance(source, single, rules, 1000000)
|
||||||
for i in 10: Parameters.advance(source, split, rules, 1.0e5)
|
for i in 10: Parameters.advance(source, split, rules, 100000)
|
||||||
check(is_equal_approx(single.trends.Integration, 0.018), "Trend integrates elapsed years and doubled stage weights")
|
check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights")
|
||||||
for key in Parameters.TRENDS:
|
for key in Parameters.TRENDS:
|
||||||
check(is_equal_approx(single.trends[key], split.trends[key]), "Trend is independent of frame partition: " + key)
|
check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key))
|
||||||
source.ethics["C2.0"].d = 1000000.0
|
source.ethics["C2.0"].d = 1000000
|
||||||
var larger: Dictionary = world.state.duplicate(true)
|
var larger: Dictionary = world.state.duplicate(true)
|
||||||
Parameters.advance(source, larger, rules, 1.0e6)
|
Parameters.advance(source, larger, rules, 1000000)
|
||||||
check(is_equal_approx(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
|
||||||
world.state.trends.Integration = 0.99
|
world.state.trends.Integration = E.of("0.99")
|
||||||
Parameters.advance(source, world.state, rules, 1.0e8)
|
Parameters.advance(source, world.state, rules, 100000000)
|
||||||
check(world.state.civilization_stats["trend:Integration"] == 2.0, "Long steps process every crossed threshold")
|
check(E.eq(world.state.civilization_stats["trend:Integration"], 2), "Long steps process every crossed threshold")
|
||||||
check(is_equal_approx(world.state.trends.Integration, 0.79), "Threshold crossing preserves the fractional remainder")
|
check(E.near(world.state.trends.Integration, E.of("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")
|
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 = bytes_to_var(var_to_bytes(world.state))
|
var restored: Dictionary = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
|
||||||
Parameters.advance(source, world.state, rules, 1.0e8)
|
Parameters.advance(source, world.state, rules, 100000000)
|
||||||
Parameters.advance(source, restored, rules, 1.0e8)
|
Parameters.advance(source, restored, rules, 100000000)
|
||||||
check(world.state == restored, "Trend events resume identically after serialization")
|
check(E.eq(world.state, restored), "Trend events resume identically after serialization")
|
||||||
Parameters.advance(source, world.state, rules, 1.0e12)
|
Parameters.advance(source, world.state, rules, E.of("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((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(world.state.world_parameters.Agg >= 1.0, "Repeated integration events cannot reduce aggression below the scale")
|
check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale")
|
||||||
var payload: Dictionary = {"format_version": 2, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
var payload: Dictionary = {"format_version": 5, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
|
||||||
var manager: Node = root.get_node("SaveManager")
|
var manager: Node = root.get_node("SaveManager")
|
||||||
check(manager.validate_data(payload) == "", "New parameters and trends pass save validation")
|
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
|
||||||
var old: Dictionary = payload.duplicate(true)
|
var old: Dictionary = payload.duplicate(true)
|
||||||
old.state.erase("world_parameters")
|
old.state.erase("world_parameters")
|
||||||
old.state.erase("trends")
|
old.state.erase("trends")
|
||||||
check(manager.validate_data(old) == "", "Existing current-format saves remain readable")
|
check(E.eq(manager.validate_data(old), ""), "Existing current-format saves remain readable")
|
||||||
payload.state.world_parameters.Agg = 20.0
|
payload.state.world_parameters.Agg = 20
|
||||||
check(manager.validate_data(payload) != "", "Out-of-range scales are rejected")
|
check(E.ne(manager.validate_data(payload), ""), "Out-of-range scales are rejected")
|
||||||
dispose(world)
|
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]]:
|
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(is_equal_approx(Parameters.density_display(sample[0]).position, sample[1]), "Density boundary " + str(sample[0]))
|
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.
|
# The fixture overrides saving/loading; the player's save is never accessed.
|
||||||
var scene = load("res://world/world.tscn").instantiate()
|
var scene = load("res://world/world.tscn").instantiate()
|
||||||
var stage = scene.get_node("Stage")
|
var stage = scene.get_node("Stage")
|
||||||
@@ -151,40 +154,36 @@ func run() -> void:
|
|||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
await process_frame
|
await process_frame
|
||||||
await process_frame
|
await process_frame
|
||||||
stage.owniverse.state.energy = Parameters.INITIAL_ENERGY / 4.0
|
stage.owniverse.state.energy = E.divide(Parameters.INITIAL_ENERGY, 4)
|
||||||
stage.owniverse.state.world_parameters.Bur = 19.0
|
stage.owniverse.state.world_parameters.Bur = 19
|
||||||
stage.owniverse.state.trends.Integration = 0.75
|
stage.owniverse.state.trends.Integration = E.of("0.75")
|
||||||
stage.world_indicators.update(stage.owniverse)
|
stage.world_indicators.update(stage.owniverse)
|
||||||
var warning: TextureRect = scene.get_node("Background/WarningBorder")
|
var warning: TextureRect = scene.get_node("Background/WarningBorder")
|
||||||
check(warning.mouse_filter == Control.MOUSE_FILTER_IGNORE, "Warning overlay does not block input")
|
check(E.eq(warning.mouse_filter, Control.MOUSE_FILTER_IGNORE), "Warning overlay does not block input")
|
||||||
var environments: Array = [
|
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"]]
|
||||||
[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"],
|
|
||||||
]
|
|
||||||
for sample in environments:
|
for sample in environments:
|
||||||
check(Parameters.density_warning_color(sample[0]).is_equal_approx(Color.html(sample[1])), "Unity warning threshold: " + str(sample[0]))
|
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 = 1e14
|
stage.owniverse.state.amount.H = E.of("1e14")
|
||||||
stage.world_indicators.update(stage.owniverse)
|
stage.world_indicators.update(stage.owniverse)
|
||||||
check(warning.self_modulate.is_equal_approx(Color.html("00c0ffff")), "Compression warning uses current universe density")
|
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.H = 0
|
||||||
stage.owniverse.state.amount.S = 1e24
|
stage.owniverse.state.amount.S = E.of("1e24")
|
||||||
stage.world_indicators.update(stage.owniverse)
|
stage.world_indicators.update(stage.owniverse)
|
||||||
check(warning.self_modulate.is_equal_approx(Color.html("ff0000ff")), "Rip warning replaces compression warning")
|
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)
|
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:
|
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
|
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(E.near(E.add(pointer.position.x, E.divide(pointer.size.x, 2)), 304), "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.eq(stage.world_indicators.trend_bars[1].scale.y, E.of("0.75")), "Trend bar displays its stored level")
|
||||||
for locale in ["en", "ru"]:
|
for locale in ["en", "ru"]:
|
||||||
global.Locale = locale
|
global.Locale = locale
|
||||||
for id in ["Energy", "Density", "Burnability", "Explosiveness", "Aggression", "Trends"]:
|
for id in ["Energy", "Density", "Burnability", "Explosiveness", "Aggression", "Trends"]:
|
||||||
var description: String = Descriptions.render(stage.owniverse, global, id, "Indicator")
|
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)
|
dispose(stage.owniverse)
|
||||||
scene.free()
|
scene.free()
|
||||||
print("World indicators: %d checks, %d failures" % [checks, failures])
|
print(("World indicators: %d checks, %d failures" % [checks, failures]))
|
||||||
quit(0 if failures == 0 else 1)
|
quit((0 if E.eq(failures, 0) else 1))
|
||||||
|
|||||||
+30
-40
@@ -1,29 +1,18 @@
|
|||||||
extends HBoxContainer
|
extends HBoxContainer
|
||||||
class_name OwniverseAction
|
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 = [
|
@onready var action_labels : Array = [$ActionControl0/Label, $ActionControl1/Label, $ActionControl2/Label, $ActionControl3/Label, $ActionControl4/Label]
|
||||||
$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 current_item_id = ""
|
||||||
|
|
||||||
var action_values: Dictionary = {
|
var action_values: Dictionary = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0}
|
||||||
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
|
current_item_id = item_id
|
||||||
var requests: Array = _get_distribution(value)
|
var requests: Array = _get_distribution(value)
|
||||||
var unique_requests: Array = []
|
var unique_requests: Array = []
|
||||||
@@ -31,32 +20,33 @@ func set_actions(item_id: String, value: float, preview: Callable = Callable())
|
|||||||
var labels: Dictionary = {}
|
var labels: Dictionary = {}
|
||||||
# Prefer the largest request (including MAX) when previews or rounded
|
# Prefer the largest request (including MAX) when previews or rounded
|
||||||
# scientific labels coincide. Keep the request paired with its preview.
|
# scientific labels coincide. Keep the request paired with its preview.
|
||||||
for i in range(requests.size() - 1, -1, -1):
|
for i in range(E.subtract(requests.size(), 1), E.neg(1), E.neg(1)):
|
||||||
if requests[i] <= 0.0: continue
|
if E.le(requests[i], 0): continue
|
||||||
var amount: float = preview.call(requests[i]) if preview.is_valid() else requests[i]
|
var amount: Variant = (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
|
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)
|
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
|
labels[text] = true
|
||||||
unique_requests.push_front(requests[i])
|
unique_requests.push_front(requests[i])
|
||||||
displayed.push_front(amount)
|
displayed.push_front(amount)
|
||||||
while unique_requests.size() < 5:
|
while E.lt(unique_requests.size(), 5):
|
||||||
unique_requests.append(0.0)
|
unique_requests.append(0)
|
||||||
displayed.append(0.0)
|
displayed.append(0)
|
||||||
_set_action_values(unique_requests, displayed)
|
_set_action_values(unique_requests, displayed)
|
||||||
|
|
||||||
func _get_distribution(value: float) -> Array:
|
func _get_distribution(value: Variant) -> Array:
|
||||||
if value < 1.0 or not is_finite(value):
|
if (E.lt(value, 1) or (not E.finite(value))):
|
||||||
return [0.0, 0.0, 0.0, 0.0, 0.0]
|
return [0, 0, 0, 0, 0]
|
||||||
value = floorf(value)
|
value = E.floor_value(value)
|
||||||
var distribution: Array = []
|
var distribution: Array = []
|
||||||
var power: float = 1.0
|
var power: Variant = 1
|
||||||
while power < value:
|
while E.lt(power, value):
|
||||||
distribution.append(power)
|
distribution.append(power)
|
||||||
if distribution.size() > 4: distribution.pop_front()
|
if E.gt(distribution.size(), 4): distribution.pop_front()
|
||||||
power *= 10.0
|
if E.ge(power, E.of("1e27")): break
|
||||||
|
power = E.multiply(power, 10)
|
||||||
distribution.append(value)
|
distribution.append(value)
|
||||||
while distribution.size() < 5: distribution.append(0.0)
|
while E.lt(distribution.size(), 5): distribution.append(0)
|
||||||
return distribution
|
return distribution
|
||||||
|
|
||||||
func _set_action_values(values: Array, displayed: Array) -> void:
|
func _set_action_values(values: Array, displayed: Array) -> void:
|
||||||
@@ -64,17 +54,17 @@ func _set_action_values(values: Array, displayed: Array) -> void:
|
|||||||
action_values[ind] = values[ind]
|
action_values[ind] = values[ind]
|
||||||
# Keep the original request for execution; show the exact planned output.
|
# Keep the original request for execution; show the exact planned output.
|
||||||
var text: String = _format_action_value(displayed[ind])
|
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].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:
|
func _format_action_value(value: Variant) -> String:
|
||||||
if value < 1.0 or not is_finite(value) or value != floorf(value):
|
if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))):
|
||||||
return ""
|
return ""
|
||||||
return Global.format_number(value)
|
return Global.format_number(value)
|
||||||
|
|
||||||
|
|
||||||
func _on_action_button_pressed(button_id: int) -> void:
|
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])
|
action_pressed.emit(page_id, action_values[button_id])
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
var energy_bars: Array = []
|
var energy_bars: Array = []
|
||||||
@@ -32,21 +33,21 @@ func _ignore_mouse_on_children(node: Node) -> void:
|
|||||||
|
|
||||||
func update(world) -> void:
|
func update(world) -> void:
|
||||||
var source: Dictionary = world.state
|
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
|
for bar in energy_bars: bar.scale.x = energy
|
||||||
var density_value: float = world.stellar_environment(source).density
|
var density_value = world.environment.stellar_environment(source).density
|
||||||
var density: Dictionary = Parameters.density_display(density_value)
|
var density: Dictionary = Parameters.density_display(density_value)
|
||||||
warning_border.self_modulate = Parameters.density_warning_color(density_value)
|
warning_border.self_modulate = Parameters.density_warning_color(density_value)
|
||||||
for indicator in density_indicators:
|
for indicator in density_indicators:
|
||||||
for mode in ["Density", "DensityRip", "DensityCrunch"]:
|
for mode in ["Density", "DensityRip", "DensityCrunch"]:
|
||||||
indicator.get_node(mode).visible = mode == density.mode
|
indicator.get_node(mode).visible = mode == density.mode
|
||||||
var pointer: Control = indicator.get_node("Pointer")
|
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
|
pointer.self_modulate = Color.BLACK if density.mode == "DensityCrunch" else Color.WHITE
|
||||||
for key in pointers:
|
for key in pointers:
|
||||||
var pointer: Control = pointers[key]
|
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():
|
for i in trend_bars.size():
|
||||||
var bar: Control = trend_bars[i]
|
var bar: Control = trend_bars[i]
|
||||||
bar.pivot_offset = Vector2(0.0, bar.size.y)
|
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))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
extends Control
|
extends Control
|
||||||
class_name OwniverseItem
|
class_name OwniverseItem
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
signal item_activated(item)
|
signal item_activated(item)
|
||||||
|
|
||||||
@@ -19,8 +20,8 @@ var item_sub_page: int = -1
|
|||||||
var item_type = ""
|
var item_type = ""
|
||||||
|
|
||||||
|
|
||||||
func set_label(value: float) -> void:
|
func set_label(value) -> void:
|
||||||
var text: String = Global.format_number(value)
|
var text: String = E.scientific(value, 5) if E.gt(E.absolute(value), 1000000000) else (E.grouped(value, 0, false) if E.ne(value, 0) else "")
|
||||||
if description.text != text:
|
if description.text != text:
|
||||||
description.text = text
|
description.text = text
|
||||||
#description.text = str(value)
|
#description.text = str(value)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Модули Owniverse
|
||||||
|
|
||||||
|
Первый этап разделения Owniverse выделяет два модуля:
|
||||||
|
|
||||||
|
- `quantity_math.gd`: нормализация положительных весов, ограниченное целочисленное распределение методом наибольших остатков, распределение с накоплением ошибки округления, накопление дробного выпуска и минимальный целый размер производственной партии.
|
||||||
|
- `entity_catalog.gd`: загрузка определений, индексы групп и событий, списки автоматических производителей и разблокируемых сущностей.
|
||||||
|
|
||||||
|
Математика не обращается к дереву сцены, Global или Owniverse. Методы накопления изменяют переданный словарь остатков; остальные методы возвращают результат. Для распределения `split` количество должно быть целым, веса — неотрицательными. Равные остатки разрешаются по строковому ID. Количества считаются целочисленно в диапазоне E27, дробные остатки хранятся в `float`. [Модуль арифметики](math/README.md) описывает допуск дробных переносов, точные отношения и формат сохранения 4.
|
||||||
|
|
||||||
|
Каталог получает словарь исходных строк через `load_definitions(rows)`, вызываемый один раз для нового экземпляра. Конструктор OwniverseEntity по-прежнему использует настройки Global. Каталог не хранит количества, партии или состояние интерфейса. Определения пока остаются Node: освобождает их владелец мира, как до рефакторинга.
|
||||||
|
|
||||||
|
Метод `members(id)` возвращает копию списка. После изменения group_as нужно вызвать `rebuild_group_index()`; добавление или удаление определения автоматически обнаруживается по изменению размера словаря. Полная перезагрузка определений выполняется созданием нового каталога.
|
||||||
|
|
||||||
|
Stage загружает каталог, затем отдельно инициализирует количество, статистику и UI. Доступ к определениям идёт через `world.catalog.entities`, к группам — через `world.catalog.members(id)`. Совместимые свойства и методы-переадресации из Owniverse удалены. Эволюция цивилизаций обращается непосредственно к QuantityMath.
|
||||||
|
|
||||||
|
Формулы, порядок обхода определений, формат сохранения и правила симуляции не изменены. Проверки модулей: `tests/test_quantity_catalog.gd`; интеграционные проверки перечислены в `tests/README.md`.
|
||||||
|
|
||||||
|
## Представление количеств и разблокировка иконок
|
||||||
|
|
||||||
|
`item_presentation.gd` получает состояние и каталог, не изменяет симуляцию и не обращается к UI или Global.
|
||||||
|
|
||||||
|
- `amounts(state, catalog, total_produced)` строит текущие или исторические количества. Текущие планеты включают свободные и связанные объекты. Подробные стадии сохраняют собственные значения одновременно с суммой count_as; готовые групповые счётчики повторно не суммируются.
|
||||||
|
- `unlocks(state, catalog, locked)` проверяет условия по историческим количествам и их отображаемым суммам. Возвращает remaining (ещё закрытые сущности) и visible (открываемые сущности и их count_as). Переданный список не изменяется. Уже открытые иконки повторно не блокируются.
|
||||||
|
- `refresh` управляет снимком pending_amounts и флагом dirty, учитывает отложенное обновление и принудительную публикацию. Сам модуль не запускает таймеры и не меняет частоту обновления.
|
||||||
|
|
||||||
|
Stage обращается непосредственно к `world.presentation`, применяет результат разблокировки к контролам и хранит список закрытых иконок. Снимки и флаги принадлежат отдельному миру и не включаются в сохранение.
|
||||||
|
|
||||||
|
## Партии, учёт, производство и эволюция
|
||||||
|
|
||||||
|
| Модуль / свойство мира | Ответственность |
|
||||||
|
| --- | --- |
|
||||||
|
| `object_batches.gd` / `batches` | Окна рождения и эволюции, частичное выбытие, остатки округления, масштабирование партий, интеграл времени жизни, прямое завершение короткоживущих партий |
|
||||||
|
| `object_ledger.gd` / `ledger` | Свободные и связанные количества, списание и зачисление, статистика выпуска, целочисленность, резервирование и передача привязок |
|
||||||
|
| `manual_production.gd` / `production` | Предпросмотр доступного количества, транзакция оплаты и привязок, ручное производство, объединение чёрных дыр в SMBH |
|
||||||
|
| `automatic_production.gd` / `automatic` | Выпуск за период и жизнь новорождённых объектов, разделение ресурсов на обычные и автоматические, энергия, квоты звёзд и бурых карликов |
|
||||||
|
| `stellar_rules.gd` / `stellar` | Продукты звёздной эволюции, создание планет, выбор звёздных преемников, два уровня разрушения и глобальные бласты |
|
||||||
|
| `life_evolution.gd` / `life` | Прогресс, деградация и повторная попытка планетарной жизни |
|
||||||
|
| `civilization_evolution.gd`, `civilization_state.gd` | Существующие правила цивилизаций: этики, войны, колонизация, лимиты и места обитания |
|
||||||
|
| `universe_environment.gd` / `environment` | Плотность, металличность, доля попаданий бластов, изменение параметров мира с сохранением возраста звёзд |
|
||||||
|
| `evolution_coordinator.gd` / `evolution` | Порядок фаз шага, последовательные поколения и публикация нового состояния |
|
||||||
|
|
||||||
|
В `owniverse.gd` остались создание состояния и модулей, скорость и проверка входного delta перед запуском эволюции. Дублирующее поле new_state и закомментированная старая реализация удалены. Единственный источник текущего состояния — `world.state`.
|
||||||
|
|
||||||
|
Внутри одной фазы снимок физических количеств переиспользуется для сверки
|
||||||
|
этик, звёздного бюджета и бюджета структур. После списания партий или переходов
|
||||||
|
создаётся новый снимок. Операции над одним типом объекта используют
|
||||||
|
`physical_amount`, не пересчитывая полную карту вселенной.
|
||||||
|
|
||||||
|
### Порядок шага
|
||||||
|
|
||||||
|
1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость.
|
||||||
|
2. Скопировать состояние, посчитать физические количества и производство за период.
|
||||||
|
3. Обработать поколения до конца интервала. Для каждого поколения: локальные разрушения → выбытие партий всех типов → план переходов → резервирование передачи привязок → зачисление преемников и их партий → производство новорождённых → применение привязок и округление → завершение переходов цивилизаций → глобальные бласты → проверка вместимости.
|
||||||
|
4. Обновить групповые количества, ограничить автоматические ресурсы и продвинуть параметры мира.
|
||||||
|
5. Опубликовать новое состояние и обновить представление количеств.
|
||||||
|
|
||||||
|
Резервирование привязок использует замороженную таблицу, чтобы одновременные переходы не зависели от порядка обработки владельцев. Коммит передач происходит после зачисления преемников. Оплата производства остаётся атомарной: при нехватке ресурсов исходное состояние не меняется.
|
||||||
|
|
||||||
|
### Зависимости и жизненный цикл
|
||||||
|
|
||||||
|
Модули симуляции получают свой Owniverse через `simulation_module.gd`. Они работают с переданным target либо с актуальным `world.state`; ссылка на старое состояние не кэшируется. Обращение к другим подсистемам явное, например `world.ledger.credit(...)` или `world.batches.append_batch(...)`. Внутренние вызовы идут напрямую.
|
||||||
|
|
||||||
|
Ссылка модуля на мир слабая, поэтому цикл владения не возникает. Owniverse владеет модулями, каждый новый мир получает собственные экземпляры. Использовать модуль после освобождения его мира нельзя. В сохранение попадает только состояние; модули, каталог и UI туда не сериализуются. Определения OwniverseEntity по-прежнему освобождает владелец сцены.
|
||||||
|
|
||||||
|
### Примеры нового API
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var available = world.production.calculate_max_producible(world.catalog.entities["YD"])
|
||||||
|
world.production.produce("YD", 10.0)
|
||||||
|
var physical = world.ledger.physical_amounts(world.state)
|
||||||
|
var displayed = world.presentation.amounts(world.state, world.catalog)
|
||||||
|
world.environment.modify_world_parameter("Bur", 1.0)
|
||||||
|
world.evolute(delta)
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверки проверяют прежние формулы через новый API; сравнение ускоренного и обычного путей выполняется через `stellar.direct_completions`. Дополнительный тест `test_module_lifecycle.gd` проверяет изоляцию миров и освобождение модулей.
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
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 E.gt(entity.lifespan, 0)):
|
||||||
|
for resource in entity.life_product:
|
||||||
|
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: Variant, to_year: Variant) -> void:
|
||||||
|
var basic: Dictionary = {}
|
||||||
|
var automatic: Dictionary = {}
|
||||||
|
for id in quantities:
|
||||||
|
var entity: OwniverseEntity = world.catalog.entities[id]
|
||||||
|
var products: Dictionary = yearly_products(entity)
|
||||||
|
if products.is_empty(): continue
|
||||||
|
var object_years: Variant = world.batches.object_years(previous, id, quantities[id], from_year, to_year)
|
||||||
|
var auto_share: Variant = E.clamp_value(entity.auto, 0, 1)
|
||||||
|
for product in products:
|
||||||
|
var amount: Variant = E.multiply(E.multiply(object_years, products[product]), WorldParameters.production(previous, entity))
|
||||||
|
basic[product] = E.add(basic.get(product, 0), E.multiply(amount, (E.subtract(1, auto_share))))
|
||||||
|
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")):
|
||||||
|
target.resource_remainders = {"basic": {}, "automatic": {}}
|
||||||
|
var pending: Dictionary = target.resource_remainders
|
||||||
|
var requests: Dictionary = {}
|
||||||
|
for id in basic:
|
||||||
|
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] = E.maximum(0, E.add(pending.basic.get(id, 0), pending.automatic.get(id, 0)))
|
||||||
|
for id in totals:
|
||||||
|
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: 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: 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)
|
||||||
|
|
||||||
|
# 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] = E.add(budget.get(id, 0), automatic[id])
|
||||||
|
for id in budget:
|
||||||
|
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), 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 E.ne(entity.group_as, "Star"):
|
||||||
|
continue
|
||||||
|
var has_cost: bool = false
|
||||||
|
for resource in entity.cost:
|
||||||
|
if E.gt(entity.cost[resource], 0):
|
||||||
|
has_cost = true
|
||||||
|
if has_cost:
|
||||||
|
candidates[id] = distribution[id]
|
||||||
|
candidates = QuantityMath.normalize_distribution(candidates)
|
||||||
|
if candidates.is_empty():
|
||||||
|
return
|
||||||
|
var costs: Dictionary = {}
|
||||||
|
var needs: Dictionary = {}
|
||||||
|
for id in candidates:
|
||||||
|
if E.le(auto_capacity(target, world.catalog.entities[id], budget), 0):
|
||||||
|
continue
|
||||||
|
for resource in world.catalog.entities[id].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] = 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 E.gt(costs[resource], 0):
|
||||||
|
scale = E.minimum(scale, E.divide(budget.get(resource, 0), costs[resource]))
|
||||||
|
for resource in needs:
|
||||||
|
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 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")):
|
||||||
|
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 (E.lt(candidates[a], candidates[b]) if E.ne(candidates[a], candidates[b]) else E.lt(a, b)))
|
||||||
|
var created: Dictionary = {}
|
||||||
|
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: Variant = world.production.production_step(entity)
|
||||||
|
if (not E.finite(step)):
|
||||||
|
continue
|
||||||
|
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)):
|
||||||
|
continue
|
||||||
|
budget = ordinary_star_budget(target)
|
||||||
|
created[id] = 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] = 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: 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: Variant = entity.cost.get("H", 0)
|
||||||
|
if E.le(cost, 0):
|
||||||
|
return
|
||||||
|
var quota: Variant = E.add(target.auto_star_deficits.get("HBD", 0), E.mul_div(E.maximum(0, new_hydrogen), E.of("0.001"), cost))
|
||||||
|
var fuel: Variant = E.maximum(0, target.auto_star_deficits.get("HBD_fuel", 0))
|
||||||
|
var available: Variant = E.minimum(target.auto_resources.get("H", 0), world.ledger.available(target, "H"))
|
||||||
|
quota = E.minimum(quota, E.divide((E.add(available, fuel)), cost))
|
||||||
|
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 (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 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 = 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
|
||||||
|
|
||||||
|
|
||||||
|
func ordinary_star_budget(target: Dictionary) -> Dictionary:
|
||||||
|
var budget: Dictionary = target.auto_resources.duplicate()
|
||||||
|
if world.catalog.entities.has("HBD"):
|
||||||
|
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) -> Variant:
|
||||||
|
var capacity: Variant = INF
|
||||||
|
var needs: Dictionary = {}
|
||||||
|
for resource in entity.cost:
|
||||||
|
var cost: Variant = E.maximum(0, entity.cost[resource])
|
||||||
|
needs[resource] = 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] = E.add(needs.get(resource, 0), E.maximum(0, entity.bind[resource]))
|
||||||
|
for resource in needs:
|
||||||
|
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 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: 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 E.le(count, 0)):
|
||||||
|
return
|
||||||
|
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: 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: 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 coefficient: Variant = E.multiply(products[resource], WorldParameters.production(target, world.catalog.entities[id]))
|
||||||
|
var output: Variant = E.multiply(object_years, coefficient)
|
||||||
|
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))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cty3i0pb8ihwf
|
||||||
@@ -1,125 +1,107 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
const State = preload("res://world/owniverse/civilization_state.gd")
|
const State = preload("res://world/owniverse/civilization_state.gd")
|
||||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
const ETHICS = State.ETHICS
|
const ETHICS = State.ETHICS
|
||||||
const OPPOSITE = {"m": "l", "l": "m", "w": "d", "d": "w", "c": "b", "b": "c"}
|
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:
|
||||||
var result: float = 0.0
|
return QuantityMath.sum(pop)
|
||||||
for n in pop.values(): result += n
|
|
||||||
return result
|
|
||||||
|
|
||||||
static func add(to: Dictionary, from: Dictionary) -> void:
|
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 = world._allocate_whole(pop, count)
|
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
|
return taken
|
||||||
|
|
||||||
static func rate(target: Dictionary, key: String, count: float, probability: float) -> float:
|
static func rate(target: Dictionary, key: String, count: Variant, probability: Variant) -> Variant:
|
||||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
|
||||||
var expected: float = count * maxf(0.0, probability) + target.civilization_remainders.get(key, 0.0)
|
return QuantityMath.accumulate_rate(target.civilization_remainders, key, count, probability)
|
||||||
var actual: float = floorf(snappedf(expected, 1e-10))
|
|
||||||
target.civilization_remainders[key] = maxf(0.0, expected - actual)
|
|
||||||
return actual
|
|
||||||
|
|
||||||
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
# Fixed-size largest-remainder apportionment with saved signed rounding errors.
|
||||||
# Runtime depends on the number of branches, never the number of civilizations.
|
# Runtime depends on the number of branches, never the number of civilizations.
|
||||||
static func split(target: Dictionary, key: String, count: float, weights: Dictionary) -> Dictionary:
|
static func split(target: Dictionary, key: String, count: Variant, weights: Dictionary) -> Dictionary:
|
||||||
var result: Dictionary = {}
|
if E.le(count, 0): return {}
|
||||||
var sum_weights: float = total(weights)
|
if (not target.has("civilization_remainders")): target.civilization_remainders = {}
|
||||||
if count <= 0.0 or sum_weights <= 0.0: return result
|
return QuantityMath.split(target.civilization_remainders, key, count, weights)
|
||||||
if not target.has("civilization_remainders"): target.civilization_remainders = {}
|
|
||||||
var expected: Dictionary = {}
|
|
||||||
var positive_sum: float = 0.0
|
|
||||||
for id in weights:
|
|
||||||
expected[id] = count * weights[id] / sum_weights + target.civilization_remainders.get(key + ":" + id, 0.0)
|
|
||||||
positive_sum += maxf(0.0, expected[id])
|
|
||||||
var order: Array = []
|
|
||||||
var assigned: float = 0.0
|
|
||||||
for id in weights:
|
|
||||||
var normalized: float = maxf(0.0, expected[id]) * count / positive_sum if positive_sum > 0.0 else 0.0
|
|
||||||
result[id] = floorf(normalized)
|
|
||||||
assigned += result[id]
|
|
||||||
order.append({"id": id, "rest": normalized - result[id]})
|
|
||||||
order.sort_custom(func(a, b): return a.rest > b.rest if a.rest != b.rest else str(a.id) < str(b.id))
|
|
||||||
for entry in order:
|
|
||||||
if assigned >= count: break
|
|
||||||
result[entry.id] += 1.0
|
|
||||||
assigned += 1.0
|
|
||||||
for id in weights:
|
|
||||||
target.civilization_remainders[key + ":" + id] = clampf(expected[id] - result[id], -1.0, 1.0)
|
|
||||||
return result
|
|
||||||
|
|
||||||
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary,
|
static func drift(target: Dictionary, key: String, pop: Dictionary, rows: Dictionary, diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
||||||
diplomacy: bool = false, colony: bool = false) -> Dictionary:
|
|
||||||
var weights: Dictionary = {}
|
var weights: Dictionary = {}
|
||||||
for ethic in pop:
|
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)
|
||||||
|
var contribution: Variant = E.multiply(pop[ethic], influence)
|
||||||
for other in ETHICS:
|
for other in ETHICS:
|
||||||
var weight: float = 9.0 if colony else 1.0
|
var weight: Variant = (9 if colony else 1)
|
||||||
if other == ethic: weight = 960.0 if colony else 196.0
|
if E.eq(other, ethic): weight = (960 if colony else 196)
|
||||||
elif other == OPPOSITE[ethic]: weight = 4.0 if colony else 0.0
|
elif E.eq(other, OPPOSITE[ethic]): weight = (4 if colony else 0)
|
||||||
weights[other] = weights.get(other, 0.0) + pop[ethic] * influence * weight
|
weights[other] = E.add(weights.get(other, 0), E.multiply(contribution, weight))
|
||||||
if total(weights) <= 0.0: return pop.duplicate()
|
if E.le(total(weights), 0): return pop.duplicate()
|
||||||
return split(target, key, total(pop), weights)
|
return split(target, key, total(pop), weights)
|
||||||
|
|
||||||
static func emit(plan: Dictionary, destination: String, pop: Dictionary) -> void:
|
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)
|
add(plan.outputs[destination], pop)
|
||||||
|
|
||||||
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
|
static func fall(target: Dictionary, plan: Dictionary, rules: Dictionary, pop: Dictionary) -> void:
|
||||||
for ethic in pop:
|
for ethic in pop:
|
||||||
|
if E.le(pop[ethic], 0): continue
|
||||||
var weights: Dictionary = {}
|
var weights: Dictionary = {}
|
||||||
for term in str(rules[plan.id].ethics[ethic].fall).split("^", false):
|
for term in str(rules[plan.id].ethics[ethic].fall).split("^", false):
|
||||||
var pair: PackedStringArray = term.split(":")
|
var pair: PackedStringArray = term.split(":")
|
||||||
weights[pair[0]] = float(pair[1])
|
weights[pair[0]] = E.of(pair[1])
|
||||||
if weights.is_empty(): weights[rules[plan.id].previous] = 1.0
|
if weights.is_empty(): weights[rules[plan.id].previous] = 1
|
||||||
var shares: Dictionary = split(target, "fall:" + plan.id + ":" + ethic, pop[ethic], weights)
|
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]})
|
for stage in shares: emit(plan, stage, {ethic: shares[stage]})
|
||||||
|
|
||||||
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary,
|
static func progress(world, target: Dictionary, rules: Dictionary, budget: Dictionary, plan: Dictionary, pop: Dictionary) -> Dictionary:
|
||||||
plan: Dictionary, pop: Dictionary) -> Dictionary:
|
|
||||||
var successor: String = rules[plan.id].next
|
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)
|
var accepted: Dictionary = take(world, pop, admitted)
|
||||||
emit(plan, successor, accepted)
|
emit(plan, successor, accepted)
|
||||||
return pop
|
return pop
|
||||||
|
|
||||||
static func new_plan(id: String, year: float) -> Dictionary:
|
static func new_plan(id: String, year: Variant) -> Dictionary:
|
||||||
return {"id": id, "from": year, "to": -INF, "amount": 0.0, "outputs": {}}
|
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:
|
static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop: Dictionary, count: Variant, year: Variant) -> Dictionary:
|
||||||
var old_total: float = total(pop)
|
var old_total: Variant = total(pop)
|
||||||
var removed: Dictionary = take(world, pop, count)
|
var removed: Dictionary = take(world, pop, count)
|
||||||
var actual: float = total(removed)
|
var actual: Variant = total(removed)
|
||||||
if actual > 0.0:
|
if E.gt(actual, 0):
|
||||||
world._scale_batches(target, plan.id, (old_total - actual) / old_total)
|
world.batches.scale_batches(target, plan.id, E.divide((E.subtract(old_total, actual)), old_total))
|
||||||
plan.amount += actual
|
plan.amount = E.add(plan.amount, actual)
|
||||||
plan.to = maxf(plan.to, year)
|
plan.to = E.maximum(plan.to, year)
|
||||||
return removed
|
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, physical: Dictionary = {}, budget_timer: RefCounted = null) -> Dictionary:
|
||||||
State.reconcile(world, target, rules)
|
var profile_started: int = world.profile_start()
|
||||||
var remaining: Dictionary = target.ethics.duplicate(true)
|
State.reconcile(world, target, rules, physical)
|
||||||
|
var remaining: Dictionary = target.ethics.duplicate()
|
||||||
|
var copied: Dictionary = {}
|
||||||
var plans: Dictionary = {}
|
var plans: Dictionary = {}
|
||||||
var colonies: Array = []
|
var colonies: Array = []
|
||||||
var collateral: Dictionary = {}
|
var collateral: Dictionary = {}
|
||||||
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
|
var modifiers: Dictionary = WorldParameters.life_modifiers(target)
|
||||||
var order: float = WorldParameters.value(target, "Order")
|
var order: Variant = WorldParameters.value(target, "Order")
|
||||||
if not target.has("civilization_stats"): target.civilization_stats = {}
|
if (not target.has("civilization_stats")): target.civilization_stats = {}
|
||||||
var ids: Array = expired.keys()
|
var ids: Array = expired.keys()
|
||||||
ids.sort()
|
ids.sort()
|
||||||
for id in ids:
|
for id in ids:
|
||||||
if not rules.has(id) or expired[id].is_empty(): continue
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
|
if ((not rules.has(id)) or expired[id].is_empty()): continue
|
||||||
var plan: Dictionary = new_plan(id, year)
|
var plan: Dictionary = new_plan(id, year)
|
||||||
plans[id] = plan
|
plans[id] = plan
|
||||||
for part: Dictionary in expired[id]:
|
for part : Dictionary in expired[id]:
|
||||||
plan.amount += part.amount
|
plan.amount = E.add(plan.amount, part.amount)
|
||||||
plan.from = minf(plan.from, part.from)
|
plan.from = E.minimum(plan.from, part.from)
|
||||||
plan.to = maxf(plan.to, part.to)
|
plan.to = E.maximum(plan.to, part.to)
|
||||||
|
remaining[id] = remaining[id].duplicate()
|
||||||
|
copied[id] = true
|
||||||
var triggered: Dictionary = take(world, remaining[id], plan.amount)
|
var triggered: Dictionary = take(world, remaining[id], plan.amount)
|
||||||
var successes: Dictionary = {}
|
var successes: Dictionary = {}
|
||||||
var failures: Dictionary = {}
|
var failures: Dictionary = {}
|
||||||
@@ -127,126 +109,147 @@ static func prepare(world, target: Dictionary, expired: Dictionary, year: float,
|
|||||||
var settlers: Dictionary = {}
|
var settlers: Dictionary = {}
|
||||||
for ethic in triggered:
|
for ethic in triggered:
|
||||||
var row: Dictionary = rules[id].ethics[ethic]
|
var row: Dictionary = rules[id].ethics[ethic]
|
||||||
var p: float = maxf(0.0, float(row.progress) * modifiers.progress)
|
var p: Variant = E.maximum(0, E.multiply(E.of(row.progress), modifiers.progress))
|
||||||
var d: float = maxf(0.0, float(row.degradation) * modifiers.degradation)
|
var d: Variant = E.maximum(0, E.multiply(E.of(row.degradation), modifiers.degradation))
|
||||||
var scale: float = maxf(1.0, p + d)
|
var scale: Variant = E.maximum(1, E.add(p, d))
|
||||||
var pro: float = rate(target, id + ":" + ethic + ":pro", triggered[ethic], p / scale)
|
var pro: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":pro"), triggered[ethic], E.divide(p, scale))
|
||||||
var deg: float = rate(target, id + ":" + ethic + ":deg", triggered[ethic], d / scale)
|
var deg: Variant = rate(target, E.add(E.add(E.add(id, ":"), ethic), ":deg"), triggered[ethic], E.divide(d, scale))
|
||||||
var bounded: Dictionary = world._allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
|
var bounded: Dictionary = QuantityMath.allocate_whole({"pro": pro, "deg": deg}, triggered[ethic])
|
||||||
if rules[id].fights: warriors[ethic] = rate(target, id + ":" + ethic + ":war", triggered[ethic], minf(1.0, float(row.war) * modifiers.war))
|
if rules[id].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, id + ":" + ethic + ":col", triggered[ethic], minf(1.0, float(row.colonization) * modifiers.colonization))
|
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
|
successes[ethic] = bounded.pro
|
||||||
failures[ethic] = bounded.deg
|
failures[ethic] = bounded.deg
|
||||||
triggered[ethic] -= bounded.pro + bounded.deg
|
triggered[ethic] = E.subtract(triggered[ethic], E.add(bounded.pro, bounded.deg))
|
||||||
|
var profile_transitions: int = world.profile_start()
|
||||||
add(triggered, progress(world, target, rules, budget, plan, successes))
|
add(triggered, progress(world, target, rules, budget, plan, successes))
|
||||||
fall(target, plan, rules, failures)
|
fall(target, plan, rules, failures)
|
||||||
|
world.profile_end("civilization_transitions", profile_transitions)
|
||||||
if rules[id].fights:
|
if rules[id].fights:
|
||||||
for ethic in warriors: warriors[ethic] = minf(warriors[ethic], triggered[ethic])
|
var profile_wars: int = world.profile_start()
|
||||||
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])
|
||||||
warriors = world._allocate_whole(warriors, wars)
|
var wars: Variant = E.minimum(total(warriors), E.floor_value(E.divide((E.add(total(triggered), total(remaining[id]))), 2)))
|
||||||
for ethic in warriors: triggered[ethic] -= warriors[ethic]
|
warriors = QuantityMath.allocate_whole(warriors, wars)
|
||||||
var outcomes: Dictionary = split(target, id + ":wars", wars, {"deg": 1.0, "pro25": 2.0, "pro50": 3.0, "pro75": 3.0})
|
for ethic in warriors: triggered[ethic] = E.subtract(triggered[ethic], warriors[ethic])
|
||||||
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 outcomes: Dictionary = split(target, E.add(id, ":wars"), wars, {"deg": 1, "pro25": 2, "pro50": 3, "pro75": 3})
|
||||||
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 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 winners: Dictionary = take(world, warriors, rate(target, id + ":wins", win_expected, 1.0))
|
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 attackers_lost: Dictionary = take(world, warriors, rate(target, id + ":attackers_lost", outcomes.get("deg", 0.0) / order, 1.0))
|
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))
|
add(triggered, progress(world, target, rules, budget, plan, winners))
|
||||||
fall(target, plan, rules, attackers_lost)
|
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 = world._allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
|
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(triggered), "later": total(remaining[id])}, losses)
|
||||||
var victims: Dictionary = take(world, triggered, sides.due)
|
var victims: Dictionary = take(world, triggered, sides.due)
|
||||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||||
fall(target, plan, rules, victims)
|
fall(target, plan, rules, victims)
|
||||||
add(triggered, warriors)
|
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)
|
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] = collateral.get(rules[id].previous, 0.0) + damage
|
collateral[rules[id].previous] = E.add(collateral.get(rules[id].previous, 0), damage)
|
||||||
target.civilization_stats.wars = target.civilization_stats.get("wars", 0.0) + wars
|
target.civilization_stats.wars = E.add(target.civilization_stats.get("wars", 0), wars)
|
||||||
|
world.profile_end("wars", profile_wars)
|
||||||
emit(plan, id, triggered)
|
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.
|
# Collateral is applied after all stages planned their own attempts.
|
||||||
for id in collateral:
|
for id in collateral:
|
||||||
if not rules.has(id): continue
|
if (not rules.has(id)): continue
|
||||||
if not plans.has(id): plans[id] = new_plan(id, year)
|
if not copied.has(id):
|
||||||
|
remaining[id] = remaining[id].duplicate()
|
||||||
|
copied[id] = true
|
||||||
|
if (not plans.has(id)): plans[id] = new_plan(id, year)
|
||||||
var plan: Dictionary = plans[id]
|
var plan: Dictionary = plans[id]
|
||||||
var retry: Dictionary = plan.outputs.get(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 = world._allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
var sides: Dictionary = QuantityMath.allocate_whole({"due": total(retry), "later": total(remaining[id])}, amount)
|
||||||
var victims: Dictionary = take(world, retry, sides.due)
|
var victims: Dictionary = take(world, retry, sides.due)
|
||||||
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
add(victims, consume_untriggered(world, target, plan, remaining[id], sides.later, year))
|
||||||
fall(target, plan, rules, victims)
|
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 incoming: Dictionary = {}
|
||||||
var planetary: Dictionary = {}
|
var planetary: Dictionary = {}
|
||||||
for id in plans:
|
for id in plans:
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return {}
|
||||||
var plan: Dictionary = plans[id]
|
var plan: Dictionary = plans[id]
|
||||||
var products: Dictionary = {}
|
var products: Dictionary = {}
|
||||||
for destination in plan.outputs:
|
for destination in plan.outputs:
|
||||||
var pop: Dictionary = plan.outputs[destination]
|
var pop: Dictionary = plan.outputs[destination]
|
||||||
if rules.has(destination):
|
if rules.has(destination):
|
||||||
if destination != "Ct":
|
if E.ne(destination, "Ct"):
|
||||||
pop = drift(target, id + ":" + destination + ":drift", pop, rules[id].ethics, destination == id and rules[id].diplomacy)
|
var profile_drift: int = world.profile_start()
|
||||||
if not incoming.has(destination): incoming[destination] = {}
|
pop = drift(target, E.add(E.add(E.add(id, ":"), destination), ":drift"), pop, rules[id].ethics, (E.eq(destination, id) and rules[id].diplomacy))
|
||||||
|
world.profile_end("ethics_drift", profile_drift)
|
||||||
|
if (not incoming.has(destination)): incoming[destination] = {}
|
||||||
add(incoming[destination], pop)
|
add(incoming[destination], pop)
|
||||||
products[destination] = total(pop)
|
products[destination] = total(pop)
|
||||||
if State.is_space_civilization(id, rules) and world.entities[destination].group_as == "Planet":
|
if (State.is_space_civilization(id, rules) and E.eq(world.catalog.entities[destination].group_as, "Planet")):
|
||||||
planetary[destination] = planetary.get(destination, 0.0) + total(pop)
|
planetary[destination] = E.add(planetary.get(destination, 0), total(pop))
|
||||||
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": maxf(plan.from, plan.to), "products": products}]
|
expired[id] = [{"amount": plan.amount, "from": plan.from, "to": E.maximum(plan.from, plan.to), "products": products}]
|
||||||
|
world.profile_end("civilization_prepare", profile_started)
|
||||||
return {"remaining": remaining, "incoming": incoming, "planetary": planetary, "colonies": colonies}
|
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, budget_timer: RefCounted = null) -> void:
|
||||||
|
var profile_started: int = world.profile_start()
|
||||||
target.ethics = transaction.remaining
|
target.ethics = transaction.remaining
|
||||||
for id in transaction.incoming:
|
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])
|
add(target.ethics[id], transaction.incoming[id])
|
||||||
# Space civilizations that fall back to planetary stages need a stellar host.
|
# Space civilizations that fall back to planetary stages need a stellar host.
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
var hosts: Dictionary = {}
|
var hosts: Dictionary = {}
|
||||||
for id in physical:
|
for id in physical:
|
||||||
if world.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:
|
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:
|
for owner in shares:
|
||||||
if not target.binds.has(owner): target.binds[owner] = {}
|
if (not target.binds.has(owner)): target.binds[owner] = {}
|
||||||
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + shares[owner]
|
target.binds[owner][id] = E.add(target.binds[owner].get(id, 0), shares[owner])
|
||||||
target.amount[id] -= shares[owner]
|
target.amount[id] = E.subtract(target.amount[id], shares[owner])
|
||||||
colonize(world, target, transaction.colonies, year, rules)
|
if budget_timer != null:
|
||||||
State.reconcile(world, target, rules)
|
await colonize(world, target, transaction.colonies, year, rules, physical, budget_timer)
|
||||||
var worship: float = 0.0
|
if budget_timer.cancelled: return
|
||||||
|
else:
|
||||||
|
colonize(world, target, transaction.colonies, year, rules, physical)
|
||||||
|
State.reconcile(world, target, rules, physical if transaction.colonies.is_empty() else {})
|
||||||
|
var worship: Variant = 0
|
||||||
for id in rules:
|
for id in rules:
|
||||||
if not rules[id].worships: continue
|
if (not rules[id].worships): continue
|
||||||
for ethic in target.ethics[id]: worship += target.ethics[id][ethic] * float(rules[id].ethics[ethic].worship)
|
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
|
target.civilization_stats.worship = worship
|
||||||
|
world.profile_end("civilization_finish", profile_started)
|
||||||
|
|
||||||
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, physical: Dictionary = {}, budget_timer: RefCounted = null) -> void:
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
if colonies.is_empty(): return
|
||||||
|
var profile_colonies: int = world.profile_start()
|
||||||
|
if physical.is_empty(): physical = world.ledger.physical_amounts(target)
|
||||||
var stock: Dictionary = {}
|
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 = {}
|
var requests: Dictionary = {}
|
||||||
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
for i in colonies.size(): requests[str(i)] = total(colonies[i].population)
|
||||||
var granted: Dictionary = world._allocate_whole(requests, total(stock))
|
var granted: Dictionary = QuantityMath.allocate_whole(requests, total(stock))
|
||||||
for i in colonies.size():
|
for i in colonies.size():
|
||||||
|
if budget_timer != null and not await budget_timer.pause(): return
|
||||||
var colony: Dictionary = colonies[i]
|
var colony: Dictionary = colonies[i]
|
||||||
var population: Dictionary = world._allocate_whole(colony.population, granted[str(i)])
|
var population: Dictionary = QuantityMath.allocate_whole(colony.population, granted[str(i)])
|
||||||
population = drift(target, colony.source + ":colonies", population, rules[colony.source].ethics, false, true)
|
population = drift(target, E.add(colony.source, ":colonies"), population, rules[colony.source].ethics, false, true)
|
||||||
var count: float = total(population)
|
var count: Variant = total(population)
|
||||||
var targets: Dictionary = world._allocate_whole(stock, count)
|
var targets: Dictionary = QuantityMath.allocate_whole(stock, count)
|
||||||
for id in targets:
|
for id in targets:
|
||||||
var amount: float = targets[id]
|
var amount: Variant = targets[id]
|
||||||
if amount <= 0.0: continue
|
if E.le(amount, 0): continue
|
||||||
var slots: Dictionary = {"": target.amount.get(id, 0.0)}
|
var slots: Dictionary = {"": target.amount.get(id, 0)}
|
||||||
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0.0)
|
for owner in target.binds: slots[owner] = target.binds[owner].get(id, 0)
|
||||||
var taken: Dictionary = world._allocate_whole(slots, amount)
|
var taken: Dictionary = QuantityMath.allocate_whole(slots, amount)
|
||||||
world._scale_batches(target, id, 1.0 - amount / stock[id])
|
world.batches.scale_batches(target, id, E.subtract(1, E.divide(amount, stock[id])))
|
||||||
stock[id] -= amount
|
stock[id] = E.subtract(stock[id], amount)
|
||||||
for owner in taken:
|
for owner in taken:
|
||||||
if taken[owner] <= 0.0: continue
|
if E.le(taken[owner], 0): continue
|
||||||
if owner == "": target.amount[id] -= taken[owner]
|
if E.eq(owner, ""): target.amount[id] = E.subtract(target.amount[id], taken[owner])
|
||||||
else: target.binds[owner][id] -= taken[owner]
|
else: target.binds[owner][id] = E.subtract(target.binds[owner][id], taken[owner])
|
||||||
var created: float = world._credit(target, colony.stage, taken[owner])
|
var created: Variant = world.ledger.credit(target, colony.stage, taken[owner])
|
||||||
if owner != "":
|
if E.ne(owner, ""):
|
||||||
target.amount[colony.stage] -= created
|
target.amount[colony.stage] = E.subtract(target.amount[colony.stage], created)
|
||||||
target.binds[owner][colony.stage] = target.binds[owner].get(colony.stage, 0.0) + created
|
target.binds[owner][colony.stage] = E.add(target.binds[owner].get(colony.stage, 0), created)
|
||||||
world._append_batch(target, colony.stage, amount, year, year)
|
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)
|
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)
|
||||||
|
world.profile_end("colonization", profile_colonies)
|
||||||
|
|||||||
@@ -1,235 +1,230 @@
|
|||||||
extends RefCounted
|
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"]
|
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
||||||
|
|
||||||
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
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;
|
# Preserve any existing ethics. New populations start equally distributed;
|
||||||
# reductions (including blasts) are proportional and never leave negative counts.
|
# reductions (including blasts) are proportional and never leave negative counts.
|
||||||
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void:
|
||||||
if not target.has("ethics"):
|
if (not target.has("ethics")):
|
||||||
target.ethics = {}
|
target.ethics = {}
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
if physical.is_empty():
|
||||||
|
physical = world.ledger.physical_amounts(target)
|
||||||
for id in rules:
|
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 current: Dictionary = target.ethics.get(id, {})
|
||||||
var total: float = 0.0
|
var total: Variant = 0
|
||||||
for ethic in ETHICS:
|
for ethic in ETHICS:
|
||||||
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
current[ethic] = E.maximum(0, current.get(ethic, 0))
|
||||||
total += current[ethic]
|
total = E.add(total, current[ethic])
|
||||||
if total > count:
|
if E.gt(total, count):
|
||||||
current = world._allocate_whole(current, count)
|
current = QuantityMath.allocate_whole(current, count)
|
||||||
elif total < count:
|
elif E.lt(total, count):
|
||||||
var missing: float = count - total
|
var missing: Variant = E.subtract(count, total)
|
||||||
var base: float = floorf(missing / ETHICS.size())
|
var base: Variant = E.floor_value(E.divide(missing, ETHICS.size()))
|
||||||
var rest: int = int(missing - base * ETHICS.size())
|
var rest: int = E.integer(E.subtract(missing, E.multiply(base, ETHICS.size())))
|
||||||
for i in 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
|
target.ethics[id] = current
|
||||||
|
|
||||||
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
|
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
|
||||||
for held: Dictionary in target.binds.values():
|
for held : Dictionary in target.binds.values():
|
||||||
for id in held:
|
for id in held:
|
||||||
if is_space_civilization(id, rules):
|
if is_space_civilization(id, rules):
|
||||||
target.amount[id] = target.amount.get(id, 0.0) + held[id]
|
target.amount[id] = E.add(target.amount.get(id, 0), held[id])
|
||||||
held[id] = 0.0
|
held[id] = 0
|
||||||
|
|
||||||
# A shared budget per stellar family, including stars bound into structures.
|
# A shared budget per stellar family, including stars bound into structures.
|
||||||
# Reservations affect capacity, not the physical stock or its production.
|
# Reservations affect capacity, not the physical stock or its production.
|
||||||
static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
static func stellar_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
if physical.is_empty():
|
||||||
|
physical = world.ledger.physical_amounts(target)
|
||||||
|
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
||||||
var capacity: Dictionary = {}
|
var capacity: Dictionary = {}
|
||||||
for id in physical:
|
for id in layout.star_homes:
|
||||||
var entity = world.entities[id]
|
for home in layout.star_homes[id]:
|
||||||
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
capacity[home] = E.add(capacity.get(home, 0), physical.get(id, 0))
|
||||||
if entity.event_group == home or entity.count_as == home or id == home:
|
capacity["SMBH"] = E.maximum(0, target.amount.get("SMBH", 0))
|
||||||
capacity[home] = capacity.get(home, 0.0) + physical[id]
|
for held : Dictionary in target.binds.values():
|
||||||
capacity["SMBH"] = maxf(0.0, target.amount.get("SMBH", 0.0))
|
capacity["SMBH"] = E.add(capacity["SMBH"], E.maximum(0, held.get("SMBH", 0)))
|
||||||
for held: Dictionary in target.binds.values():
|
|
||||||
capacity["SMBH"] += maxf(0.0, held.get("SMBH", 0.0))
|
|
||||||
var populations: Dictionary = {}
|
var populations: Dictionary = {}
|
||||||
var requested: Dictionary = {}
|
var requested: Dictionary = {}
|
||||||
for id in rules:
|
for id in rules:
|
||||||
if not is_space_civilization(id, rules):
|
if (not is_space_civilization(id, rules)):
|
||||||
continue
|
continue
|
||||||
populations[id] = physical.get(id, 0.0)
|
populations[id] = physical.get(id, 0)
|
||||||
var home: String = rules[id].home
|
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 = {}
|
var free: Dictionary = {}
|
||||||
for home in capacity:
|
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)}
|
return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules, physical)}
|
||||||
|
|
||||||
# Account for the source's existing reservation when changing stages. A failed
|
# Account for the source's existing reservation when changing stages. A failed
|
||||||
# transition retains its population at the source, rather than losing objects.
|
# transition retains its population at the source, rather than losing objects.
|
||||||
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String,
|
static func admit_transition(budget: Dictionary, rules: Dictionary, source: String, destination: String, requested: Variant) -> Variant:
|
||||||
destination: String, requested: float) -> float:
|
if (E.eq(source, destination) or (not is_space_civilization(destination, rules))):
|
||||||
if source == destination or not is_space_civilization(destination, rules):
|
|
||||||
return requested
|
return requested
|
||||||
var home: String = rules[destination].home
|
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_home: String = ""
|
||||||
var old_cost: float = 0.0
|
var old_cost: Variant = 0
|
||||||
if is_space_civilization(source, rules):
|
if is_space_civilization(source, rules):
|
||||||
old_home = rules[source].home
|
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.
|
# Existing overcapacity is handled by the separate population policy.
|
||||||
if budget.requested.get(home, 0.0) > budget.capacity.get(home, 0.0):
|
if E.gt(budget.requested.get(home, 0), budget.capacity.get(home, 0)):
|
||||||
return 0.0
|
return 0
|
||||||
var net_cost: float = cost - old_cost if old_home == home else cost
|
var net_cost: Variant = (E.subtract(cost, old_cost) if E.eq(old_home, home) else cost)
|
||||||
var admitted: float = floorf(maxf(0.0, requested))
|
var admitted: Variant = E.floor_value(E.maximum(0, requested))
|
||||||
if net_cost > 0.0:
|
if E.gt(net_cost, 0):
|
||||||
admitted = minf(admitted, floorf(budget.free.get(home, 0.0) / net_cost))
|
admitted = E.minimum(admitted, E.floor_value(E.divide(budget.free.get(home, 0), net_cost)))
|
||||||
if budget.has("habitats"):
|
if budget.has("habitats"):
|
||||||
var housing: Dictionary = budget.habitats
|
var housing: Dictionary = budget.habitats
|
||||||
var new_req: Dictionary = housing.requirements.get(destination, {})
|
var new_req: Dictionary = housing.requirements.get(destination, {})
|
||||||
var old_req: Dictionary = housing.requirements.get(source, {})
|
var old_req: Dictionary = housing.requirements.get(source, {})
|
||||||
if not new_req.is_empty():
|
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)
|
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 net_units > 0.0:
|
if E.gt(net_units, 0):
|
||||||
var free_units: float = maxf(0.0, housing.capacity.get(new_req.id, 0.0) - housing.requested.get(new_req.id, 0.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 = minf(admitted, floorf(free_units / net_units))
|
admitted = E.minimum(admitted, E.floor_value(E.divide(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 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] = housing.requested.get(new_req.id, 0.0) + admitted * new_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 old_home != "":
|
if E.ne(old_home, ""):
|
||||||
budget.requested[old_home] = maxf(0.0, budget.requested.get(old_home, 0.0) - admitted * old_cost)
|
budget.requested[old_home] = E.maximum(0, E.subtract(budget.requested.get(old_home, 0), E.multiply(admitted, old_cost)))
|
||||||
budget.free[old_home] = maxf(0.0, budget.capacity.get(old_home, 0.0) - budget.requested[old_home])
|
budget.free[old_home] = E.maximum(0, E.subtract(budget.capacity.get(old_home, 0), budget.requested[old_home]))
|
||||||
budget.requested[home] = budget.requested.get(home, 0.0) + admitted * cost
|
budget.requested[home] = E.add(budget.requested.get(home, 0), E.multiply(admitted, cost))
|
||||||
budget.free[home] = maxf(0.0, budget.capacity.get(home, 0.0) - budget.requested[home])
|
budget.free[home] = E.maximum(0, E.subtract(budget.capacity.get(home, 0), budget.requested[home]))
|
||||||
return admitted
|
return admitted
|
||||||
|
|
||||||
# Resolve shortages from the most advanced stages downwards. This gives existing
|
# Resolve shortages from the most advanced stages downwards. This gives existing
|
||||||
# advanced populations priority and lets a demotion cascade in the same cycle.
|
# 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:
|
||||||
|
var profile_started: int = world.profile_start()
|
||||||
detach_space_civilizations(target, rules)
|
detach_space_civilizations(target, rules)
|
||||||
reconcile(world, target, rules)
|
var physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
var budget: Dictionary = stellar_budget(world, target, rules)
|
reconcile(world, target, rules, physical)
|
||||||
|
var budget: Dictionary = stellar_budget(world, target, rules, physical)
|
||||||
var remaining: Dictionary = budget.capacity.duplicate()
|
var remaining: Dictionary = budget.capacity.duplicate()
|
||||||
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
|
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
|
||||||
var stages: Array = []
|
var stages: Array = world.catalog.civilization_layout(rules).stages
|
||||||
for id in rules:
|
var changed: bool = false
|
||||||
if is_space_civilization(id, rules):
|
for index in range(E.subtract(stages.size(), 1), E.neg(1), E.neg(1)):
|
||||||
stages.append(id)
|
|
||||||
stages.sort()
|
|
||||||
for index in range(stages.size() - 1, -1, -1):
|
|
||||||
var id: String = stages[index]
|
var id: String = stages[index]
|
||||||
|
var count: Variant = target.amount.get(id, 0)
|
||||||
|
if E.le(count, 0): continue
|
||||||
var home: String = rules[id].home
|
var home: String = rules[id].home
|
||||||
var cost: float = maxf(1.0, rules[id].stars)
|
var cost: Variant = E.maximum(1, rules[id].stars)
|
||||||
var count: float = target.amount.get(id, 0.0)
|
var supported: Variant = E.minimum(count, E.floor_value(E.divide(remaining.get(home, 0), cost)))
|
||||||
var supported: float = minf(count, floorf(remaining.get(home, 0.0) / cost))
|
|
||||||
var requirement: Dictionary = budget.habitats.requirements.get(id, {})
|
var requirement: Dictionary = budget.habitats.requirements.get(id, {})
|
||||||
if not requirement.is_empty():
|
if (not requirement.is_empty()):
|
||||||
supported = minf(supported, floorf(remaining_homes.get(requirement.id, 0.0) / requirement.units))
|
supported = E.minimum(supported, E.floor_value(E.divide(remaining_homes.get(requirement.id, 0), requirement.units)))
|
||||||
remaining_homes[requirement.id] = maxf(0.0, remaining_homes.get(requirement.id, 0.0) - supported * requirement.units)
|
remaining_homes[requirement.id] = E.maximum(0, E.subtract(remaining_homes.get(requirement.id, 0), E.multiply(supported, requirement.units)))
|
||||||
remaining[home] = maxf(0.0, remaining.get(home, 0.0) - supported * cost)
|
remaining[home] = E.maximum(0, E.subtract(remaining.get(home, 0), E.multiply(supported, cost)))
|
||||||
var excess: float = count - supported
|
var excess: Variant = E.subtract(count, supported)
|
||||||
if excess <= 0.0:
|
if E.le(excess, 0):
|
||||||
continue
|
continue
|
||||||
var previous: String = stages[index - 1] if index > 0 else "C1.9"
|
changed = true
|
||||||
world._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
|
target.amount[id] = supported
|
||||||
world._credit(target, previous, excess)
|
world.ledger.credit(target, previous, excess)
|
||||||
world._append_batch(target, previous, excess, year, year)
|
world.batches.append_batch(target, previous, excess, year, year)
|
||||||
var removed: Dictionary = world._allocate_whole(target.ethics[id], excess)
|
var removed: Dictionary = QuantityMath.allocate_whole(target.ethics[id], excess)
|
||||||
if not target.ethics.has(previous):
|
if (not target.ethics.has(previous)):
|
||||||
target.ethics[previous] = {}
|
target.ethics[previous] = {}
|
||||||
for ethic in ETHICS:
|
for ethic in ETHICS:
|
||||||
target.ethics[id][ethic] -= removed[ethic]
|
target.ethics[id][ethic] = E.subtract(target.ethics[id][ethic], removed[ethic])
|
||||||
target.ethics[previous][ethic] = target.ethics[previous].get(ethic, 0.0) + removed[ethic]
|
target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic])
|
||||||
if previous == "C1.9":
|
if E.eq(previous, "C1.9"):
|
||||||
var stars: Dictionary = {}
|
var stars: Dictionary = {}
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
var current_physical: Dictionary = world.ledger.physical_amounts(target)
|
||||||
for star in physical:
|
for star in current_physical:
|
||||||
if world.entities[star].event_group == "DwStar" and physical[star] > 0.0:
|
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)):
|
||||||
stars[star] = physical[star]
|
stars[star] = current_physical[star]
|
||||||
var weights: Dictionary = world._normalize_distribution(stars)
|
var weights: Dictionary = QuantityMath.normalize_distribution(stars)
|
||||||
# Worlds are aggregates: a star may host several inhabited planets.
|
# Worlds are aggregates: a star may host several inhabited planets.
|
||||||
var assigned: float = 0.0
|
var assigned: Variant = 0
|
||||||
var owners: Array = weights.keys()
|
var owners: Array = weights.keys()
|
||||||
owners.sort()
|
owners.sort()
|
||||||
for i in owners.size():
|
for i in owners.size():
|
||||||
var owner: String = owners[i]
|
var owner: String = owners[i]
|
||||||
var amount: float = floorf(excess * weights[owner]) if i + 1 < owners.size() else excess - assigned
|
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 += amount
|
assigned = E.add(assigned, amount)
|
||||||
if not target.binds.has(owner):
|
if (not target.binds.has(owner)):
|
||||||
target.binds[owner] = {}
|
target.binds[owner] = {}
|
||||||
target.binds[owner][previous] = target.binds[owner].get(previous, 0.0) + amount
|
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
|
||||||
target.amount[previous] -= amount
|
target.amount[previous] = E.subtract(target.amount[previous], amount)
|
||||||
reconcile(world, target, rules)
|
if changed:
|
||||||
assign_homes(world, target, rules)
|
physical = world.ledger.physical_amounts(target)
|
||||||
|
reconcile(world, target, rules, physical)
|
||||||
|
assign_homes(world, target, rules, physical)
|
||||||
|
world.profile_end("enforce_capacity", profile_started)
|
||||||
|
|
||||||
# Unity's structure chain: direct Claim resets the required structure count;
|
# Unity's structure chain: direct Claim resets the required structure count;
|
||||||
# intervening stages require ten times more units of the preceding structure.
|
# intervening stages require ten times more units of the preceding structure.
|
||||||
static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary:
|
static func habitat_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
|
||||||
var physical: Dictionary = world._physical_amounts(target)
|
if physical.is_empty():
|
||||||
var structures: Array = []
|
physical = world.ledger.physical_amounts(target)
|
||||||
|
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
||||||
|
var structures: Array = layout.structures
|
||||||
var capacity: Dictionary = {}
|
var capacity: Dictionary = {}
|
||||||
for id in world.entities:
|
for id in structures: capacity[id] = physical.get(id, 0)
|
||||||
if world.entities[id].claim != "":
|
var requirements: Dictionary = layout.requirements
|
||||||
structures.append(id)
|
var anchors: Dictionary = layout.anchors
|
||||||
capacity[id] = physical.get(id, 0.0)
|
|
||||||
structures.sort_custom(func(a, b):
|
|
||||||
var left: float = world.entities[a].civ_bonus
|
|
||||||
var right: float = world.entities[b].civ_bonus
|
|
||||||
return left > right if left != right else str(a) < str(b))
|
|
||||||
var stages: Array = []
|
|
||||||
for id in rules:
|
|
||||||
if is_space_civilization(id, rules): stages.append(id)
|
|
||||||
stages.sort()
|
|
||||||
var requirements: Dictionary = {}
|
|
||||||
var anchors: Dictionary = {}
|
|
||||||
var anchor: String = ""
|
|
||||||
var units: float = 1.0
|
|
||||||
for id in stages:
|
|
||||||
var direct: String = ""
|
|
||||||
for structure in structures:
|
|
||||||
if world.entities[structure].claim == id: direct = structure
|
|
||||||
if direct != "":
|
|
||||||
anchor = direct
|
|
||||||
units = 1.0
|
|
||||||
else:
|
|
||||||
units *= 10.0
|
|
||||||
anchors[id] = anchor
|
|
||||||
if id.begins_with("C3.") or id.begins_with("C4."):
|
|
||||||
requirements[id] = {"id": anchor, "units": units}
|
|
||||||
var requested: Dictionary = {}
|
var requested: Dictionary = {}
|
||||||
for id in requirements:
|
for id in requirements:
|
||||||
var req: Dictionary = requirements[id]
|
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}
|
return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors}
|
||||||
|
|
||||||
static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void:
|
# A structure's direct reservation belongs only to its Claim stage. Other
|
||||||
var budget: Dictionary = habitat_budget(world, target, rules)
|
# stages do not debit it, so each stage's placement can be calculated alone.
|
||||||
var used: Dictionary = {}
|
static func stage_homes(world, target: Dictionary, id: String, layout: Dictionary, capacity: Dictionary = {}) -> Dictionary:
|
||||||
target.civilization_homes = {}
|
var left: Variant = target.amount.get(id, 0)
|
||||||
var stages: Array = budget.anchors.keys()
|
var homes: Dictionary = {}
|
||||||
stages.sort()
|
if E.le(left, 0): return homes
|
||||||
stages.reverse()
|
var allowed: bool = layout.anchors[id] == ""
|
||||||
for id in stages:
|
for structure in layout.structures:
|
||||||
var left: float = target.amount.get(id, 0.0)
|
if structure == layout.anchors[id]: allowed = true
|
||||||
var homes: Dictionary = {}
|
if not allowed: continue
|
||||||
var allowed: bool = budget.anchors[id] == ""
|
var stock: Variant = capacity.get(structure, 0) if not capacity.is_empty() else world.ledger.physical_amount(target, structure)
|
||||||
for structure in budget.structures:
|
if E.le(stock, 0): continue
|
||||||
if structure == budget.anchors[id]: allowed = true
|
var amount: Variant = left
|
||||||
if not allowed or budget.capacity[structure] <= 0.0: continue
|
if world.catalog.entities[structure].claim == id:
|
||||||
var amount: float = left
|
amount = E.minimum(amount, stock)
|
||||||
if world.entities[structure].claim == id:
|
if E.gt(amount, 0): homes[structure] = amount
|
||||||
amount = minf(amount, maxf(0.0, budget.capacity[structure] - used.get(structure, 0.0)))
|
left = E.subtract(left, amount)
|
||||||
used[structure] = used.get(structure, 0.0) + amount
|
if E.le(left, 0): break
|
||||||
if amount > 0.0: homes[structure] = amount
|
return homes
|
||||||
left -= amount
|
|
||||||
if left <= 0.0: break
|
|
||||||
target.civilization_homes[id] = homes
|
|
||||||
|
|
||||||
static func effective_span(world, target: Dictionary, id: String, rules: Dictionary) -> float:
|
static func assign_homes(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void:
|
||||||
if not rules.has(id): return world.entities[id].lifespan
|
var profile_started: int = world.profile_start()
|
||||||
var base: float = rules[id].span
|
var layout: Dictionary = world.catalog.civilization_layout(rules)
|
||||||
if not is_space_civilization(id, rules): return base
|
var capacity: Dictionary = {}
|
||||||
assign_homes(world, target, rules)
|
for structure in layout.structures:
|
||||||
var population: float = target.amount.get(id, 0.0)
|
capacity[structure] = world.ledger.physical_amount(target, structure) if physical.is_empty() else physical.get(structure, 0)
|
||||||
if population <= 0.0: return base
|
target.civilization_homes = {}
|
||||||
var weighted: float = population
|
for index in range(layout.stages.size() - 1, -1, -1):
|
||||||
for home in target.civilization_homes.get(id, {}):
|
var id: String = layout.stages[index]
|
||||||
weighted += target.civilization_homes[id][home] * (maxf(0.0, world.entities[home].civ_bonus) - 1.0)
|
target.civilization_homes[id] = stage_homes(world, target, id, layout, capacity)
|
||||||
return base * population / weighted if weighted > 0.0 else base
|
world.profile_end("assign_homes", profile_started)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
var population: Variant = target.amount.get(id, 0)
|
||||||
|
if E.le(population, 0): return base
|
||||||
|
var homes: Dictionary = stage_homes(world, target, id, world.catalog.civilization_layout(rules))
|
||||||
|
target.civilization_homes[id] = homes
|
||||||
|
var weighted: Variant = population
|
||||||
|
for home in homes:
|
||||||
|
weighted = E.add(weighted, E.multiply(homes[home], (E.subtract(E.maximum(0, world.catalog.entities[home].civ_bonus), 1))))
|
||||||
|
return (E.mul_div(base, population, weighted) if E.gt(weighted, 0) else base)
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
## Definitions and their indexes only: no population, batches or UI state.
|
||||||
|
## Definitions are Nodes owned by the caller, as before this extraction.
|
||||||
|
|
||||||
|
var entities: Dictionary[String, OwniverseEntity] = {}
|
||||||
|
var groups: Dictionary[String, Array] = {}
|
||||||
|
var event_groups: Dictionary[String, Array] = {}
|
||||||
|
var automatic_producers: Array = []
|
||||||
|
var unlockable_entities: Array = []
|
||||||
|
var _group_members: Dictionary = {}
|
||||||
|
var index_revision: int = 0
|
||||||
|
var _indexed_entity_count: int = -1
|
||||||
|
var _civilization_layout: Dictionary = {}
|
||||||
|
var _civilization_rules: Dictionary = {}
|
||||||
|
|
||||||
|
## Populate an empty catalog; replacement uses a new catalog to preserve ownership.
|
||||||
|
func load_definitions(rows: Dictionary) -> void:
|
||||||
|
assert(entities.is_empty(), "Load definitions into an empty catalog")
|
||||||
|
for id in rows:
|
||||||
|
var entity := OwniverseEntity.new(rows[id])
|
||||||
|
entities[id] = entity
|
||||||
|
if not entity.unlock.is_empty(): unlockable_entities.append(entity)
|
||||||
|
if entity.product_per_year_type != "": automatic_producers.append(entity)
|
||||||
|
if entity.group_as != "":
|
||||||
|
if not groups.has(entity.group_as): groups[entity.group_as] = []
|
||||||
|
groups[entity.group_as].append(id)
|
||||||
|
if entity.event_group != "":
|
||||||
|
if not event_groups.has(entity.event_group): event_groups[entity.event_group] = []
|
||||||
|
event_groups[entity.event_group].append(id)
|
||||||
|
rebuild_group_index()
|
||||||
|
|
||||||
|
## Call explicitly after modifying group_as without changing the entity count.
|
||||||
|
func rebuild_group_index() -> void:
|
||||||
|
index_revision += 1
|
||||||
|
_civilization_layout.clear()
|
||||||
|
_group_members.clear()
|
||||||
|
for id in entities:
|
||||||
|
var group: String = entities[id].group_as
|
||||||
|
if not _group_members.has(group): _group_members[group] = []
|
||||||
|
_group_members[group].append(id)
|
||||||
|
_indexed_entity_count = entities.size()
|
||||||
|
|
||||||
|
func _ensure_group_index() -> void:
|
||||||
|
if _indexed_entity_count != entities.size(): rebuild_group_index()
|
||||||
|
|
||||||
|
func is_group(id: String) -> bool:
|
||||||
|
_ensure_group_index()
|
||||||
|
return groups.has(id) or _group_members.has(id)
|
||||||
|
|
||||||
|
func members(id: String) -> Array:
|
||||||
|
if not is_group(id): return [id]
|
||||||
|
return _group_members.get(id, []).duplicate()
|
||||||
|
|
||||||
|
## Definition-only cache. Rebuild indexes after changing claims or bonuses;
|
||||||
|
## invalidate this layout after changing stage homes in an existing rules table.
|
||||||
|
func invalidate_civilization_layout() -> void:
|
||||||
|
_civilization_layout.clear()
|
||||||
|
|
||||||
|
func civilization_layout(rules: Dictionary) -> Dictionary:
|
||||||
|
_ensure_group_index()
|
||||||
|
if not _civilization_layout.is_empty() and is_same(_civilization_rules, rules):
|
||||||
|
return _civilization_layout
|
||||||
|
_civilization_rules = rules
|
||||||
|
var structures: Array = []
|
||||||
|
for id in entities:
|
||||||
|
if entities[id].claim != "": structures.append(id)
|
||||||
|
structures.sort_custom(func(a, b): return E.gt(entities[a].civ_bonus, entities[b].civ_bonus) if E.ne(entities[a].civ_bonus, entities[b].civ_bonus) else str(a) < str(b))
|
||||||
|
var stages: Array = []
|
||||||
|
for id in rules:
|
||||||
|
if str(rules[id].home) != "": stages.append(id)
|
||||||
|
stages.sort()
|
||||||
|
var anchors: Dictionary = {}
|
||||||
|
var requirements: Dictionary = {}
|
||||||
|
var anchor: String = ""
|
||||||
|
var units: Variant = 1
|
||||||
|
for id in stages:
|
||||||
|
var direct: String = ""
|
||||||
|
for structure in structures:
|
||||||
|
if entities[structure].claim == id: direct = structure
|
||||||
|
if direct != "":
|
||||||
|
anchor = direct
|
||||||
|
units = 1
|
||||||
|
else:
|
||||||
|
units = E.multiply(units, 10)
|
||||||
|
anchors[id] = anchor
|
||||||
|
if id.begins_with("C3.") or id.begins_with("C4."):
|
||||||
|
requirements[id] = {"id": anchor, "units": units}
|
||||||
|
var star_homes: Dictionary = {}
|
||||||
|
for id in entities:
|
||||||
|
var homes: Array = []
|
||||||
|
for home in ["DwStar", "GiStar", "WD", "NS", "MGT", "BH"]:
|
||||||
|
if entities[id].event_group == home or entities[id].count_as == home or id == home:
|
||||||
|
homes.append(home)
|
||||||
|
if not homes.is_empty(): star_homes[id] = homes
|
||||||
|
_civilization_layout = {"structures": structures, "stages": stages, "anchors": anchors, "requirements": requirements, "star_homes": star_homes}
|
||||||
|
return _civilization_layout
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6g3m8wmm0ms7
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
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")
|
||||||
|
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
const QuantityMath = preload("res://world/owniverse/quantity_math.gd")
|
||||||
|
|
||||||
|
func initialize_life_batches() -> void:
|
||||||
|
CivilizationState.detach_space_civilizations(world.state, Global.CivilizationRules)
|
||||||
|
CivilizationState.enforce_capacity(world, world.state, Global.CivilizationRules, world.state.time.datetime)
|
||||||
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
|
for id in world.catalog.entities:
|
||||||
|
if world.catalog.entities[id].life_rule.is_empty():
|
||||||
|
continue
|
||||||
|
var tracked: Variant = 0
|
||||||
|
for batch : Dictionary in world.state.batches.get(id, []):
|
||||||
|
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 run_frame(clock: Dictionary, budget: RefCounted) -> void:
|
||||||
|
var _keep_alive: RefCounted = self
|
||||||
|
await evolute_to_datetime(clock.datetime, clock, budget)
|
||||||
|
if not budget.cancelled and world != null: world.finish_frame()
|
||||||
|
|
||||||
|
func evolute_to_datetime(datetime: Variant, clock: Dictionary = {}, budget: RefCounted = null) -> void:
|
||||||
|
var _keep_alive: RefCounted = self
|
||||||
|
# Reconcile only when simulation advances.
|
||||||
|
# Fractional frame updates cannot change capacity or populations.
|
||||||
|
var processed_year: Variant = SimulationTime.processed_year(datetime)
|
||||||
|
if E.gt(processed_year, world.state.time.year):
|
||||||
|
var profile_initialize: int = world.profile_start()
|
||||||
|
initialize_life_batches()
|
||||||
|
world.profile_end("initialize", profile_initialize)
|
||||||
|
var previous: Dictionary = world.state
|
||||||
|
var next: Dictionary = previous.duplicate_deep()
|
||||||
|
next.version = SimulationTime.STATE_VERSION
|
||||||
|
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
|
||||||
|
|
||||||
|
var profile_production: int = world.profile_start()
|
||||||
|
world.automatic.produce_for_period(previous, next, quantities, distribution, from_year, to_year)
|
||||||
|
world.profile_end("production", profile_production)
|
||||||
|
var profile_generations: int = world.profile_start()
|
||||||
|
if budget != null:
|
||||||
|
await evolve_batches(next, to_year, budget)
|
||||||
|
if budget.cancelled: return
|
||||||
|
else:
|
||||||
|
evolve_batches(next, to_year)
|
||||||
|
world.profile_end("generations", profile_generations)
|
||||||
|
world.ledger.refresh_groups(next)
|
||||||
|
world.ledger.clamp_auto_resources(next)
|
||||||
|
var profile_trends: int = world.profile_start()
|
||||||
|
WorldParameters.advance(previous, next, Global.CivilizationRules, E.subtract(to_year, from_year))
|
||||||
|
world.profile_end("trends", profile_trends)
|
||||||
|
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 = 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: Variant, budget: RefCounted = null) -> void:
|
||||||
|
var _keep_alive: RefCounted = self
|
||||||
|
# A long cycle may contain several generations; preserve actual birth intervals.
|
||||||
|
var completed: Dictionary = {}
|
||||||
|
while await evolve_batch_generation(target, to_year, completed, budget):
|
||||||
|
if budget != null and not await budget.pause(): return
|
||||||
|
|
||||||
|
|
||||||
|
func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Dictionary = { }, budget: RefCounted = null) -> bool:
|
||||||
|
var _keep_alive: RefCounted = self
|
||||||
|
# Apply direct completions at the original next-generation boundary, so
|
||||||
|
# binding transfers, the blast environment and capacity checks keep order.
|
||||||
|
if budget != null and not await budget.pause(): return false
|
||||||
|
var completed_now: Dictionary = completed.duplicate(true)
|
||||||
|
completed.clear()
|
||||||
|
var quantities: Dictionary = world.ledger.physical_amounts(target)
|
||||||
|
var expired: Dictionary = {}
|
||||||
|
var preview: Dictionary = {"batches": {}}
|
||||||
|
for id in target.batches:
|
||||||
|
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 E.le(world.catalog.entities[id].destroy_level, 0)):
|
||||||
|
continue
|
||||||
|
var dying: Variant = 0
|
||||||
|
for part : Dictionary in completed_now.get(id, []):
|
||||||
|
dying = E.add(dying, part.amount)
|
||||||
|
for part : Dictionary in world.batches.withdraw_batches(preview, id, to_year):
|
||||||
|
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] = 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()):
|
||||||
|
has_expired = true
|
||||||
|
break
|
||||||
|
if (not has_expired):
|
||||||
|
return false
|
||||||
|
var fractions: Dictionary = {}
|
||||||
|
var outputs: Dictionary = {}
|
||||||
|
var current_quantities: Dictionary = quantities
|
||||||
|
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities)
|
||||||
|
var civilization_transaction: Dictionary = await CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities, budget)
|
||||||
|
if budget != null and budget.cancelled: return false
|
||||||
|
for id in expired:
|
||||||
|
if budget != null and not await budget.pause(): return false
|
||||||
|
var total: Variant = 0
|
||||||
|
for part : Dictionary in expired[id]:
|
||||||
|
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")):
|
||||||
|
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: Variant = part.products[successor]
|
||||||
|
var admitted: Variant = CivilizationState.admit_transition(star_budget, Global.CivilizationRules, id, successor, requested)
|
||||||
|
part.products[successor] = 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] = E.add(combined.get(successor, 0), part.products[successor])
|
||||||
|
outputs[id] = {}
|
||||||
|
if E.gt(total, 0):
|
||||||
|
for successor in combined:
|
||||||
|
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: 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:
|
||||||
|
if budget != null and not await budget.pause(): return false
|
||||||
|
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)):
|
||||||
|
continue
|
||||||
|
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 produces: bool = not world.automatic.yearly_products(world.catalog.entities[successor]).is_empty()
|
||||||
|
var before_batches: Array = target.batches.get(successor, []).duplicate(true) if produces else []
|
||||||
|
var cutoff: Variant = (to_year if world.stellar.uses_short_lived_fast_path(successor) else E.neg(INF))
|
||||||
|
var profile_newborn: int = world.profile_start()
|
||||||
|
var integral: Variant
|
||||||
|
if budget != null:
|
||||||
|
integral = await world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff, budget)
|
||||||
|
if budget.cancelled: return false
|
||||||
|
else:
|
||||||
|
integral = world.batches.append_batch(target, successor, amount, part.from, part.to, completed, cutoff)
|
||||||
|
if produces:
|
||||||
|
world.automatic.produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches, integral)
|
||||||
|
world.profile_end("newborn_batches", profile_newborn)
|
||||||
|
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)
|
||||||
|
if budget != null:
|
||||||
|
await CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules, budget)
|
||||||
|
if budget.cancelled: return false
|
||||||
|
else:
|
||||||
|
CivilizationEvolution.finish(world, target, civilization_transaction, to_year, Global.CivilizationRules)
|
||||||
|
world.stellar.apply_blast(target, blast_power, environment)
|
||||||
|
CivilizationState.enforce_capacity(world, target, Global.CivilizationRules, to_year)
|
||||||
|
return true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bok6yra83r83c
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Cooperative work on the main thread. No partially calculated state is published.
|
||||||
|
var tree: SceneTree
|
||||||
|
var slice_usec: int = 40000
|
||||||
|
var deadline: int
|
||||||
|
var cancelled: bool = false
|
||||||
|
var waiting_usec: int = 0
|
||||||
|
|
||||||
|
func _init(scene_tree: SceneTree, milliseconds: int = 40):
|
||||||
|
tree = scene_tree
|
||||||
|
slice_usec = milliseconds * 1000
|
||||||
|
deadline = Time.get_ticks_usec() + slice_usec
|
||||||
|
|
||||||
|
func pause() -> bool:
|
||||||
|
var keep_alive: RefCounted = self
|
||||||
|
if cancelled: return false
|
||||||
|
if Time.get_ticks_usec() >= deadline:
|
||||||
|
var waiting_from: int = Time.get_ticks_usec()
|
||||||
|
await tree.process_frame
|
||||||
|
keep_alive.waiting_usec += Time.get_ticks_usec() - waiting_from
|
||||||
|
deadline = Time.get_ticks_usec() + slice_usec
|
||||||
|
return not keep_alive.cancelled
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://hu57pwad02sg
|
||||||
@@ -1,101 +1,90 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
const E = preload("res://world/owniverse/math/exact.gd")
|
||||||
|
|
||||||
const GREEN = "#40cc40"
|
const GREEN = "#40cc40"
|
||||||
const RED = "#ff6060"
|
const RED = "#ff6060"
|
||||||
const GREY = "#888888"
|
const GREY = "#888888"
|
||||||
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
const Parameters = preload("res://world/owniverse/world_parameters.gd")
|
||||||
|
|
||||||
static func number(value: float, digits: int = 0) -> String:
|
static func number(value, digits: int = 0) -> String:
|
||||||
if not is_finite(value):
|
if not E.finite(value): return "—"
|
||||||
return "—"
|
return E.grouped(value, digits, false)
|
||||||
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 colored(text: String, color: String) -> String:
|
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:
|
static func matches(world, id: String) -> Array:
|
||||||
var result: Array = []
|
var result: Array = []
|
||||||
for key in world.entities:
|
for key in world.catalog.entities:
|
||||||
var entity = world.entities[key]
|
var entity = world.catalog.entities[key]
|
||||||
if key == id or entity.count_as == id or entity.group_as == id:
|
if ((E.eq(key, id) or E.eq(entity.count_as, id)) or E.eq(entity.group_as, id)):
|
||||||
if not world._is_group(key):
|
if (not world.catalog.is_group(key)):
|
||||||
result.append(key)
|
result.append(key)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
static func quantities(world, id: String) -> Dictionary:
|
static func quantities(world, id: String) -> Dictionary:
|
||||||
var free: float = 0.0
|
var free: Variant = 0
|
||||||
var total: float = 0.0
|
var total: Variant = 0
|
||||||
var physical: Dictionary = world._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
for key in matches(world, id):
|
for key in matches(world, id):
|
||||||
free += world.state.amount.get(key, 0.0)
|
free = E.add(free, world.state.amount.get(key, 0))
|
||||||
total += physical.get(key, 0.0)
|
total = E.add(total, physical.get(key, 0))
|
||||||
if id == "SMBH":
|
if E.eq(id, "SMBH"):
|
||||||
free = world.state.amount.get(id, 0.0)
|
free = world.state.amount.get(id, 0)
|
||||||
total = free
|
total = free
|
||||||
return {"free": free, "total": total}
|
return {"free": free, "total": total}
|
||||||
|
|
||||||
static func costs(world, global: Node, id: String) -> String:
|
static func costs(world, global: Node, id: String) -> String:
|
||||||
if not world.entities.has(id):
|
if (not world.catalog.entities.has(id)):
|
||||||
return ""
|
return ""
|
||||||
var lines: PackedStringArray = []
|
var lines: PackedStringArray = []
|
||||||
for requirements in [world.entities[id].cost, world.entities[id].bind]:
|
for requirements in [world.catalog.entities[id].cost, world.catalog.entities[id].bind]:
|
||||||
for resource in requirements:
|
for resource in requirements:
|
||||||
var required: float = requirements[resource]
|
var required: Variant = requirements[resource]
|
||||||
if required < 0.0:
|
if E.lt(required, 0):
|
||||||
continue
|
continue
|
||||||
var available: float = floorf(world._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))
|
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)
|
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 counts: Dictionary = quantities(world, id)
|
||||||
var show_total: bool = false
|
var show_total: bool = false
|
||||||
for key in matches(world, id):
|
for key in matches(world, id):
|
||||||
show_total = show_total or world.entities[key].group_as == "Planet"
|
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.entities.has(id) else "—"
|
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:
|
if ((not show_total) and E.gt(counts.total, counts.free)):
|
||||||
amount += " " + colored("(" + number(counts.total) + ")", GREY)
|
amount = E.add(amount, E.add(" ", colored(E.add(E.add("(", number(counts.total)), ")"), GREY)))
|
||||||
if rate != 0.0:
|
if E.ne(rate, 0):
|
||||||
amount += " | " + colored(("−" if rate < 0.0 else "+") + number(absf(rate), 1), RED if rate < 0.0 else GREEN)
|
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._physical_amounts(world.state)
|
var physical: Dictionary = world.ledger.physical_amounts(world.state)
|
||||||
var voids: float = 0.0
|
var voids: Variant = 0
|
||||||
for key in physical:
|
for key in physical:
|
||||||
voids += physical[key] * world.entities[key].voids
|
voids = E.add(voids, E.multiply(physical[key], world.catalog.entities[key].voids))
|
||||||
var he: float = world.state.amount.get("He", 0.0)
|
var he: Variant = world.state.amount.get("He", 0)
|
||||||
var h: float = world.state.amount.get("H", 0.0)
|
var h: Variant = world.state.amount.get("H", 0)
|
||||||
var mixture: String = "—"
|
var mixture: String = "—"
|
||||||
if he + h > 0.0:
|
if E.gt(E.add(he, h), 0):
|
||||||
mixture = "He: " + number(100.0 * he / (he + h), 2) + "% / H: " + number(100.0 * h / (he + h), 2) + "%"
|
mixture = E.add(E.add(E.add(E.add("He: ", number(E.mul_div(100, he, (E.add(he, h))), 2)), "% / H: "), number(E.mul_div(100, h, (E.add(he, h))), 2)), "%")
|
||||||
var environment: Dictionary = world.stellar_environment(world.state)
|
var environment: Dictionary = world.environment.stellar_environment(world.state)
|
||||||
return {
|
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": "—"}
|
||||||
"amount": amount, "colored costs": costs(world, global, id),
|
|
||||||
"lifespan": number(world.entities[id].lifespan) if world.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": "—",
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
var values: Dictionary = variables(world, global, id, rate)
|
||||||
if category == "Indicator":
|
if E.eq(category, "Indicator"):
|
||||||
if id == "Density":
|
if E.eq(id, "Density"):
|
||||||
id = Parameters.density_display(world.stellar_environment(world.state).density).mode
|
id = Parameters.density_display(world.environment.stellar_environment(world.state).density).mode
|
||||||
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
|
var scales: Dictionary = {"Burnability": "Bur", "Explosiveness": "Exp", "Aggression": "Agg"}
|
||||||
if scales.has(id):
|
if scales.has(id):
|
||||||
var level: float = Parameters.value(world.state, scales[id])
|
var level: Variant = Parameters.value(world.state, scales[id])
|
||||||
values.amount = number(level, 1 if level != floorf(level) else 0)
|
values.amount = number(level, (1 if E.ne(level, E.floor_value(level)) else 0))
|
||||||
values.scale = global.render_template(global.get_template("Scale" + str(clampi(roundi(level), 1, 19)), "Indicator", "Scale"), {})
|
values.scale = global.render_template(global.get_template(E.add("Scale", str(clampi(E.integer(E.round_value(level)), 1, 19))), "Indicator", "Scale"), {})
|
||||||
if id == "Trends":
|
if E.eq(id, "Trends"):
|
||||||
for trend in Parameters.TRENDS:
|
for trend in Parameters.TRENDS:
|
||||||
values[trend.to_lower()] = number(world.state.get("trends", {}).get(trend, 0.0) * 100.0, 2) + "%"
|
values[trend.to_lower()] = E.add(number(E.multiply(world.state.get("trends", {}).get(trend, 0), 100), 2), "%")
|
||||||
if id == "Energy":
|
if E.eq(id, "Energy"):
|
||||||
values["энергия"] = number(world.state.energy) + " (" + number(clampf(world.state.energy / Parameters.INITIAL_ENERGY, 0.0, 1.0) * 100.0, 2) + "%)"
|
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 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)
|
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)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user