17 Commits

Author SHA1 Message Date
Kirill e94093ac17 Add Android production release and testing builds workflows
Testing builds / test-and-build (push) Successful in 2m36s
2026-10-02 00:07:34 +03:00
Kirill c4e7302567 Add new tests and implement frame budget management
- Introduced `civilization_benchmark.gd` and `legacy_quantity_math.gd` for benchmarking and legacy quantity calculations.
- Created `test_dynamic_math.gd`, `test_frame_evolution.gd`, and `test_sparse_batches.gd` to validate mathematical operations and frame evolution logic.
- Implemented `frame_budget.gd` to manage cooperative work on the main thread, ensuring no partially calculated state is published.
- Added UID files for all new scripts to maintain unique identifiers.
2026-10-01 23:43:34 +03:00
Kirill f51eea6c0f Enhance simulation performance and accuracy by optimizing physical amount calculations and refining arithmetic operations 2026-10-01 20:30:53 +03:00
Kirill befb377774 Add new music tracks to the sound directory
- Added cosmic_unicorns.ogg
- Added funky_eldritch.ogg
- Added meditative_beings.ogg
- Added owniverse_main_theme.ogg

These tracks enhance the audio experience of the project.
2026-10-01 07:48:52 +03:00
Kirill fad862d618 Add benchmark for saved state performance
- Introduced `benchmark_saved_state.gd` to evaluate the performance of the saved state restoration process.
- Implemented JSON integer restoration to handle large integers accurately.
- Added command-line argument parsing for specifying the path to the diagnostic JSON file and optional delta seconds.
- Conducted performance tests at different speeds and logged the elapsed time for six consecutive steps.
- Created a new UID file for the benchmark script.
2026-09-30 22:46:26 +03:00
Kirill fb8a3f5658 Add exact arithmetic implementation and tests
- Introduced exact arithmetic with support for large integers and fractions in `exact.gd`, `big_integer.gd`, and `exact_value.gd`.
- Implemented a comprehensive test suite in `test_exact_math.gd` to validate arithmetic operations against expected results.
- Added a README in `math` directory to document the exact arithmetic system and its usage.
- Ensured that all arithmetic operations maintain precision without floating-point errors, using a custom representation for large numbers.
- Included methods for encoding and decoding exact values for serialization.
2026-09-30 08:08:27 +03:00
Kirill f2111a376a Add core simulation modules for owniverse mechanics
- Introduced `life_evolution.gd` to manage life evolution processes.
- Implemented `manual_production.gd` for handling production mechanics, including resource management and production steps.
- Created `object_batches.gd` to manage batches of objects, including scaling and appending batches.
- Developed `object_ledger.gd` for tracking physical amounts, available resources, and binding ownerships.
- Added `simulation_module.gd` as a base class for simulation modules, providing a weak reference to the world.
- Established `stellar_rules.gd` for managing stellar evolution and destruction mechanics.
- Introduced `universe_environment.gd` to calculate environmental parameters based on the current state of the universe.
2026-09-29 20:23:20 +03:00
Kirill da03ba11c0 Refactor Owniverse modules for quantity mathematics and entity catalog
- Introduced `quantity_math.gd` for arithmetic operations including allocation, normalization, and accumulation of quantities without scene tree dependencies.
- Created `entity_catalog.gd` to manage entity definitions, groups, and event indexes, ensuring separation from simulation state and UI.
- Implemented `item_presentation.gd` for read-only projections of simulation state, handling amounts and unlock conditions for UI elements.
- Updated `civilization_evolution.gd` and `civilization_state.gd` to utilize new quantity math functions, improving code clarity and maintainability.
- Enhanced tests in `test_quantity_catalog.gd` to validate new functionality and ensure correctness of mathematical operations.
- Revised documentation to reflect changes in module structure and functionality.
2026-09-29 19:55:20 +03:00
Kirill c2313e9163 Enhance item visibility logic and update initial energy parameter; add tests for life icon functionality 2026-09-28 22:56:40 +03:00
Kirill 2f0a3ee8d4 Warming border added 2026-09-28 22:32:52 +03:00
Kirill 15ee7833ed Add world indicators and parameters to enhance universe simulation
- new universe creation function.
2026-09-28 22:19:13 +03:00
Kirill e79f3fa362 Fix highlight visibility toggle and enhance color modulation logic 2026-09-22 23:54:13 +03:00
Kirill e87d4083e9 Refactor code structure for improved readability and maintainability 2026-09-22 23:47:59 +03:00
Kirill 5a18b41528 Refactor code structure for improved readability and maintainability 2026-09-22 23:10:09 +03:00
Kirill d6fd40969f Implement short-lived stellar event handling and optimize batch processing; add tests for new functionality 2026-09-22 22:50:54 +03:00
Kirill a3d24114a1 Add screen management to top menu and update button functionality; replace close button with back button 2026-09-21 22:32:56 +03:00
Kirill 8867da569a Refactor code structure and remove redundant sections for improved readability and maintainability 2026-09-21 21:46:43 +03:00
209 changed files with 9089 additions and 4312 deletions
+107
View File
@@ -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
-94
View File
@@ -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"
+62
View File
@@ -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
View File
@@ -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:
+165 -128
View File
@@ -1,17 +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 STATE_VERSION: String = SimulationTime.STATE_VERSION const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
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.
@@ -20,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
@@ -56,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:
@@ -69,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:
@@ -98,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 ""
@@ -143,157 +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", # Optional additions to the current format. Saves predating indicators keep
"binds", "amount", "distribution", "auto_resources", "auto_star_deficits", # their quantities and receive neutral defaults from Owniverse on load.
"production_remainders", "resource_remainders", "evolution_remainders", "life_remainders", for field in saved:
"planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders", if (not E.contains(required, field) and not E.contains(["world_parameters", "trends"], field)):
"civilization_stats", "civilization_homes", "ethics", "stats", return "unsupported state structure"
] if saved.has("world_parameters"):
if saved.size() != required.size(): if ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())):
return "unsupported state structure" return "invalid state.world_parameters"
for key in WorldParameters.defaults():
if ((not saved.world_parameters.has(key)) or E.lt(saved.world_parameters[key], 1)):
return E.add("invalid world parameter ", key)
if (E.contains(["Bur", "Exp", "Agg"], key) and E.gt(saved.world_parameters[key], 19)):
return E.add("invalid world scale ", key)
if saved.has("trends"):
if ((not _number_map(saved.trends)) or E.ne(saved.trends.size(), WorldParameters.TRENDS.size())):
return "invalid state.trends"
for key in WorldParameters.TRENDS:
if ((not saved.trends.has(key)) or E.ge(saved.trends[key], 1)):
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 ""
@@ -304,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)
@@ -312,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])
+36
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://bfe1fyutcw0cx
+8 -8
View File
@@ -21,14 +21,14 @@ LRG|0||||50||50000000||||S:300||DSN^SGRB%0.1|2|||RG|Star|||||
HRG|0||||100||10000000||||S:400.||SN^GRB%1|2|||RG|Star||||| HRG|0||||100||10000000||||S:400.||SN^GRB%1|2|||RG|Star|||||
RSG|0||||300||1000000||||S:500||BSN^LGRB%3|2|||RG|Star||||| RSG|0||||300||1000000||||S:500||BSN^LGRB%3|2|||RG|Star|||||
RHG|0||||1000||100000||||S:600||HN^HGRB%10|2|||RG|Star||||| RHG|0||||1000||100000||||S:600||HN^HGRB%10|2|||RG|Star|||||
DSN|0||||||1|||2|S:59.1||SNS:1&H:6.5&He:13.5&Met:11&blast:2|2|mass:15&H:6.5&HE:13.5&MET:11||||SNGRBB|||| DSN|0||||||1|||2|S:59.1||SNS:1&H:6.5&He:13.5&Met:11|2|mass:15&H:6.5&HE:13.5&MET:11||||SNGRBB||||
SN|0||||||1|||9|S:99.1||LNS:1&H:8&He:50&Met:11&blast:9|2|mass:18&H:8&HE:50&MET:11||||SNGRBB|||| SN|0||||||1|||9|S:99.1||LNS:1&H:8&He:50&Met:11|2|mass:18&H:8&HE:50&MET:11||||SNGRBB||||
BSN|0||||||1||.|305|S:309.1||MGT:1&H:15&He:186&Met:15&blast:305|2|mass:21&H:15&HE:186&MET:15||||SNGRBB|||| BSN|0||||||1||.|305|S:309.1||MGT:1&H:15&He:186&Met:15|2|mass:21&H:15&HE:186&MET:15||||SNGRBB||||
HN|0||||||1|||9990|S:939.1||BH40:1&H:30&He:560&Met:30&blast:9990|2|mass:40&H:30&HE:560&MET:30||||SNGRBB|||| HN|0||||||1|||9990|S:939.1||BH40:1&H:30&He:560&Met:30|2|mass:40&H:30&HE:560&MET:30||||SNGRBB||||
SGRB|0||||||1||0.001|31|S:149.1||BH25:1&H:0.5&He:10&Met:4&blast:31|2|mass:25&H:0.5&HE:10&MET:4||||SNGRBB|||| SGRB|0||||||1||0.001|31|S:149.1||BH25:1&H:0.5&He:10&Met:4|2|mass:25&H:0.5&HE:10&MET:4||||SNGRBB||||
GRB|0||||||1||0.01|136|S:239.1||BH30:1&H:19&He:12&Met:5&blast:136|2|mass:30&H:19&HE:12&MET:5||||SNGRBB|||| GRB|0||||||1||0.01|136|S:239.1||BH30:1&H:19&He:12&Met:5|2|mass:30&H:19&HE:12&MET:5||||SNGRBB||||
LGRB|0||||||1||0.03|3446|S:669.1||BH40:1&H:68&He:18&Met:9&blast:3446|2|mass:40&H:68&HE:18&MET:9||||SNGRBB|||| LGRB|0||||||1||0.03|3446|S:669.1||BH40:1&H:68&He:18&Met:9|2|mass:40&H:68&HE:18&MET:9||||SNGRBB||||
HGRB|0||||||1||0.1|97342|S:1939.1||BH60:1&H:0&He:40&Met:10&blast:97342|2|mass:60&H:0&HE:40&MET:10||||SNGRBB|||| HGRB|0||||||1||0.1|97342|S:1939.1||BH60:1&H:0&He:40&Met:10|2|mass:60&H:0&HE:40&MET:10||||SNGRBB||||
CBD|0||||0.1||||||||||||BD|||||| CBD|0||||0.1||||||||||||BD||||||
WD|1|WD:1||||||||||||||||||||| WD|1|WD:1|||||||||||||||||||||
TWD|0||||0.8||||||||||||WD|Star|Degen|||| TWD|0||||0.8||||||||||||WD|Star|Degen||||
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
uid://cunl2sqmcj1ff
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
[remap]
importer="keep"
-3
View File
@@ -1,3 +0,0 @@
[remap]
importer="skip"
+15
View File
@@ -0,0 +1,15 @@
[gd_resource type="AudioBusLayout" format=3 uid="uid://b5g7p85k5do0m"]
[resource]
bus/1/name = &"Music"
bus/1/solo = false
bus/1/mute = false
bus/1/bypass_fx = false
bus/1/volume_db = 0.0
bus/1/send = &"Master"
bus/2/name = &"FX"
bus/2/solo = false
bus/2/mute = false
bus/2/bypass_fx = false
bus/2/volume_db = 0.0
bus/2/send = &"Master"
+77 -4
View File
@@ -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 сохраняется.
## Порядок расчёта ## Порядок расчёта
@@ -27,6 +34,18 @@
## Постепенные партии ## Постепенные партии
### Короткоживущие звёздные события внутри длинного шага
Для SN, DSN, BSN, HN и GRB-событий полностью завершившиеся окна рождения не создают отдельные словари партий. Их целые количества суммируются в очередь завершений текущего расчёта; обычные партии сохраняются только для окон, срок которых ещё не закончился. Состояние этой очереди не записывается в сохранение: она полностью обрабатывается до публикации нового состояния.
Окно завершено, если `window_end + lifespan ≤ to_year`. Его доля количества рассчитывается прежним методом разности накопленных целых значений. Для производства пространства интеграл исчезновения вычитается по окнам в прежнем порядке, чтобы сохранить округление. Здесь остаётся лёгкий арифметический проход по окнам, но отсутствуют создание, копирование, сортировка и последующая обработка тысяч временных словарей.
Ресурсы, остатки звёзд и бласты от завершившейся части применяются на следующем поколении расчёта, как раньше. Поэтому сохраняются порядок передачи привязок, среда бластов и ограничения цивилизаций. Ускорение допускается только для событий группы `SNGRBB` с линейным выходом и без преемников с ненулевым сроком жизни. При изменении таблиц остальные случаи автоматически используют обычный путь. Сроки жизни и ширина окон не увеличиваются, формат сохранения не меняется.
На сохранении возраста 140,55 млн лет, при шестой скорости и шаге 1/60 секунды: максимальное временное число партий SN/GRB уменьшилось с 1668 до 2; средний расчёт кадра без отрисовки — примерно с 15 до 10 мс. За 120 шагов результаты старого и нового путей совпали по всем полям состояния. Эти значения характеризуют конкретное сохранение, а не гарантированную частоту кадров на всех устройствах.
### Общие окна рождения и эволюции
Окно рождения шириной `w = lifespan / 40` при `lifespan > 10`, иначе `w = lifespan`. Окно превращения сдвинуто на `lifespan`. При частичном списании: Окно рождения шириной `w = lifespan / 40` при `lifespan > 10`, иначе `w = lifespan`. Окно превращения сдвинуто на `lifespan`. При частичном списании:
``` ```
@@ -60,7 +79,7 @@ GI не хранит накопленные счётчики первичной
- `density = 1.23456e-7 * sqrt(B + M) / max(1, V)^(1/6)`. - `density = 1.23456e-7 * sqrt(B + M) / max(1, V)^(1/6)`.
- `blast_factor = clamp(free_S / V, 0, 1)`; при нулевом объёме `1`. - `blast_factor = clamp(free_S / V, 0, 1)`; при нулевом объёме `1`.
Система сил Unity и планетные бонусы в GI пока не перенесены; здесь используются нейтральные множители 1. Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов. Планетные бонусы сил пока не перенесены. Параметры мира Bur/Exp/Agg имеют собственные сохраняемые шкалы и эффективные множители (см. раздел индикаторов ниже). Металличность для производства фиксируется до расходования ресурсов на звёзды. Среда бласта фиксируется для поколения до добавления его продуктов.
## Разрушение и бласты ## Разрушение и бласты
@@ -72,7 +91,7 @@ GI не хранит накопленные счётчики первичной
Уровень 1 превращает только `blastability=1` в RP (HP, EON, цивилизации до C1.2). C1.3 и выше защищены от этого уровня. Уровень 2 уничтожает все планеты, включая защищённые цивилизации, без создания RP. Уничтоженные объекты удаляются из жизненных партий, чтобы не возникать заново. Уровень 1 превращает только `blastability=1` в RP (HP, EON, цивилизации до C1.2). C1.3 и выше защищены от этого уровня. Уровень 2 уничтожает все планеты, включая защищённые цивилизации, без создания RP. Уничтоженные объекты удаляются из жизненных партий, чтобы не возникать заново.
`blast` в выходе — событие, а не предмет. Поле `blast` сущности повторно не суммируется с выходом `blast:N`. Единственный источник мощности — поле `blast` сущности: `power = expired_count × max(0, entity.blast)`. При `blast = 0` внешнего воздействия нет. Старые вхождения `blast:N` в `output` игнорируются, включая альтернативные выходы; двойного суммирования нет. Внутренний выход `blast` остаётся событием, а не предметом. `destroy_level` независимо задаёт уровень локального разрушения у умирающего объекта; внешние бласты всегда действуют на уровне 1.
## Границы этапа ## Границы этапа
@@ -90,7 +109,7 @@ GI не хранит накопленные счётчики первичной
Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются. Прогресс и деградация округляются вниз с сохранением остатков. Если оба накопленных исхода конкурируют за последнюю планету, распределяются только существующие объекты, а неиспользованный остаток переносится на следующую попытку. Деградация распределяется по весам `fall`; знаковые ошибки распределения также сохраняются.
Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются; агрессия, порядок и просвещение пока имеют нейтральное влияние. Для всех шести стадий базовые доли за попытку — `progress=0.00025`, `degradation=0.0025`. Интервалы HP/EON1/EON2/EON3/EON4/EON5: 5 млн, 10 млн, 10 млн, 1 млн, 1 млн, 500 тыс. лет. Используются постепенные окна GI шириной `span / 40`. Случайные множители Unity не применяются. Агрессия, порядок и просвещение изменяют вероятности по формулам из раздела индикаторов ниже.
HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком. HP деградирует в RP. У эонов возможен откат к предыдущим стадиям по таблице. RP не развивается сам. C0.0 и C0.1 также развиваются: период 2000 лет, прогресс 0.0175, деградация 0.025. Переходы C0.0 → C0.1 → C0.2, откат C0.0 → EON5 и C0.1 → C0.0. Развитие C0.2 и выше подключено через этические правила. Конфликт civ_bonus устранён повторным импортом: бонусы структур совпадают с Unity. Для существующих этических цивилизаций пользователь выбрал равномерное распределение между шестью этиками с детерминированным целым остатком.
@@ -132,4 +151,58 @@ HP деградирует в RP. У эонов возможен откат к п
Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса. Размещение выбирает структуры в порядке убывания `civ_bonus`, начиная с соответствующего стадии `claim`. Прямое размещение ограничено числом структур; остальные разрешённые структуры при наличии дают общий бонус, как в Unity. Период новых партий равен `span / средневзвешенный civ_bonus`, для неразмещённой части множитель равен 1. Уже назначенные сроки партий не переписываются при изменении бонуса.
`life_version = 3` добавляет часы существующим C0.2+ с момента загрузки; старые часы HP, эонов и начальных цивилизаций сохраняются. Ct не получает часы. Миграция выполняется однократно. Глобальные силы и их модификаторы не входят в этот перенос: используются нейтральные множители 1. Ct не получает часы. Инициализация отсутствующих партий жизни выполняется при загрузке и перед продвижением симуляции. Параметры мира и тенденции описаны ниже; интерфейс применения сил в этот перенос не входит.
## Параметры мира, тенденции и индикаторы
Источник формул — Unity `OwniverseInstance`, `Life`, `LifeActions`, `Trends` и классы индикаторов. Расчёты находятся в `world_parameters.gd`; показ — в `ui/indicators/world_indicators.gd`. Шкалы обновляются каждые 0,1 реальной секунды и сразу при создании/загрузке вселенной. Подписи используют текущую локаль и шаблоны.
### Энергия и плотность
Энергия сохраняет экономику GI: запас расходуется на H/S. Заполнение индикатора — `clamp(energy / 1e14, 0, 1)`. Это не коэффициент производства Unity; дополнительное замедление производства при уменьшении запаса не вводится. В описании выводятся остаток и процент.
Плотность берётся из `stellar_environment`. Положение указателя ограничено диапазоном 0…1:
- `density ≤ 1e-5`: шкала разрыва, `(log10(max(density, 1e-7)) + 7) / 2`;
- `1e-5 < density < 0.9`: обычная шкала, `ln(density × 1e5) / ln(9e4)`;
- `density ≥ 0.9`: шкала сжатия, `(density − 0.9) × 10`.
В Unity в первой формуле было `−7`; это ошибка, не позволяющая указателю двигаться. Здесь исправлено на `+7`. Переключение шкалы меняет и заголовок/описание индикатора. Само наступление апокалипсиса индикатор не запускает.
Общая рамка `Background/WarningBorder` получает цвета Unity `WarningUI` в формате RGBA: при плотности ≤1e-5 — `C040407F`, ≤1e-6 — `DF2020BF`, ≤1e-7 — `FF0000FF`; при плотности ≥0,9 — `40A0C07F`, ≥0,97 — `20B0DFBF`, ≥1 — `00C0FFFF`. В нормальном диапазоне рамка прозрачна. Она обновляется вместе с индикаторами, не перехватывает нажатия и снимается при восстановлении плотности. Необратимая последовательность апокалипсиса Unity сюда не входит.
### Горючесть, взрывчатость и агрессия
`world_parameters` хранит Bur, Exp и Agg в диапазоне 1…19, начальное значение — 10. Эффективный множитель `M(x) = 10^((x−10)/9)`: крайние значения 0,1 и 10, нейтральное — 1.
- Bur делит срок жизни новых звёзд на `M(Bur)`. `modify_world_parameter("Bur", delta)` также пересчитывает оставшееся время существующих звёзд с коэффициентом `старый M / новый M`, сохраняя количество и остатки округления. Производство звёзд умножается на `M`, если `M ≤ 1`, иначе на `sqrt(M)`.
- Exp умножает вероятность альтернативного исхода (с ограничением до 1), мощность `entity.blast` и производство событий группы SNGRBB. Локальный `destroy_level` от него не меняется.
- Для Agg обозначим `a = M(Agg)`. Вероятность прогресса умножается на `a`, деградации — на `(1 − (1−a)/20) / Order`, войны — на `1 − (1−a)/5`, колонизации — на `1 − (1−a)/2`. Если сумма прогресса и деградации превышает 1, они пропорционально нормируются. Вероятности войны/колонизации ограничены единицей.
- Для первичной жизни дополнительный множитель прогресса — Enlightenment. Order дополнительно уменьшает потери в войнах и побочный ущерб. Начальные Enlightenment и Order равны 1.
Изменять шкалы следует через `owniverse.modify_world_parameter(key, delta)`, чтобы горючесть корректно обновляла партии. Готовых кнопок применения сил этот метод не добавляет.
### Тенденции
Пять направлений: Antitheism, Integration, Xenocide, Reconstruction, Spirituality. Участвуют космические стадии с непустым `home`; вес первой — 1, каждой следующей — вдвое больше. Для каждой тенденции:
`rate = Σ_стадий (вес стадии × Σ_этик(доля этики × вес этики))`
`прирост = rate × прошедшие игровые годы × 1e-9`.
Доли берутся на начало интервала, после проверки вместимости. Умножение на время исправляет ошибку Unity, где аргумент period не использовался. Накопление само по себе не зависит от FPS; изменение состава цивилизаций внутри крупных шагов остаётся дискретным, как остальная симуляция.
Веса этик в порядке пяти направлений:
| Этика | Антитеизм | Интеграция | Ксеноцид | Реконструкция | Духовность |
|---|---:|---:|---:|---:|---:|
| m | 0 | 2 | 2 | 0 | 6 |
| l | 6 | 1 | 1 | 2 | 0 |
| w | 2 | 0 | 6 | 0 | 2 |
| d | 0 | 6 | 0 | 1 | 3 |
| c | 1 | 3 | 3 | 3 | 0 |
| b | 3 | 0 | 0 | 6 | 1 |
При достижении 1 учитываются все целые срабатывания, дробный остаток сохраняется. Только Integration имеет последствия в исходной Unity: равновероятно выбирается Federation (+0,1 и Agg −1), Enlightenment (+0,2 и Agg −1) или Order (+0,1). Генератор сохраняет своё состояние. До 256 событий за шаг выбор выполняется по одному; для больших количеств используется нормальная аппроксимация мультиномиального распределения с сохранением точного суммарного числа событий, чтобы высокие скорости не блокировали кадр. Она может менять последовательность исходов при другом разбиении больших интервалов.
Federation сохраняется и увеличивается как в Unity, но в исходном коде не применяется к дипломатии. Дополнительное правило для него не придумано. Остальные четыре тенденции имеют счётчики срабатываний и сброс, но не последствия — так же, как в исходной реализации. Параметры и остатки тенденций сохраняются; у существующего сохранения текущего формата без этих полей используются нейтральные значения и нулевые тенденции, без изменения ресурсов или возраста.
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.
+202 -149
View File
@@ -1,10 +1,14 @@
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")
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png" const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
const ItemDescription = preload("res://world/owniverse/item_description.gd") const ItemDescription = preload("res://world/owniverse/item_description.gd")
const WorldIndicators = preload("res://ui/indicators/world_indicators.gd")
var world_indicators: RefCounted
var indicator_elapsed: float = 0.0
var description_targets: Dictionary = {} var description_targets: Dictionary = {}
var description_samples: Dictionary = {} var description_samples: Dictionary = {}
var description_rates: Dictionary = {} var description_rates: Dictionary = {}
@@ -16,51 +20,59 @@ const PAGE_WIDTH = 1080
const DRAG_TOLERANCE = 360 const DRAG_TOLERANCE = 360
const SWIPE_DURATION: float = 0.4 const SWIPE_DURATION: float = 0.4
@onready var stage_page_scroller: ScrollContainer = $StageRows/PageScroller @onready var stage_page_scroller: ScrollContainer = $World/PageScroller
@onready var item_pages: Array[Control] = [ @onready var item_pages: Array[Control] = [
$StageRows/PageScroller/HBoxContainer/ForcePage/Forces, $World/PageScroller/HBoxContainer/ForcePage/Forces,
$StageRows/PageScroller/HBoxContainer/ResourcePage/HBoxContainer/Resources, $World/PageScroller/HBoxContainer/ResourcePage/HBoxContainer/Resources,
$StageRows/PageScroller/HBoxContainer/StarPage/Stars, $World/PageScroller/HBoxContainer/StarPage/Stars,
$StageRows/PageScroller/HBoxContainer/StructurePage/Structures, $World/PageScroller/HBoxContainer/StructurePage/Structures,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Life, $World/PageScroller/HBoxContainer/LifePage/Planets/Life,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/LivingPlanet, $World/PageScroller/HBoxContainer/LifePage/Planets/LivingPlanet,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ0, $World/PageScroller/HBoxContainer/LifePage/Planets/Civ0,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ1, $World/PageScroller/HBoxContainer/LifePage/Planets/Civ1,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ2, $World/PageScroller/HBoxContainer/LifePage/Planets/Civ2,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ3, $World/PageScroller/HBoxContainer/LifePage/Planets/Civ3,
$StageRows/PageScroller/HBoxContainer/LifePage/Planets/Civ4 $World/PageScroller/HBoxContainer/LifePage/Planets/Civ4
] ]
@onready var page_actions: Dictionary[int, OwniverseAction] = { @onready var page_actions: Dictionary[int, OwniverseAction] = {
2: $StageRows/PageScroller/HBoxContainer/StarPage/Actions, 2: $World/PageScroller/HBoxContainer/StarPage/Actions,
3: $StageRows/PageScroller/HBoxContainer/StructurePage/Actions, 3: $World/PageScroller/HBoxContainer/StructurePage/Actions,
} }
@onready var g_item_descriptions: Array = [ @onready var g_item_descriptions: Array = [
$StageRows/PageScroller/HBoxContainer/ForcePage/Description, $World/PageScroller/HBoxContainer/ForcePage/Description,
$StageRows/PageScroller/HBoxContainer/ResourcePage/Description, $World/PageScroller/HBoxContainer/ResourcePage/Description,
$StageRows/PageScroller/HBoxContainer/StarPage/Description, $World/PageScroller/HBoxContainer/StarPage/Description,
$StageRows/PageScroller/HBoxContainer/StructurePage/Description, $World/PageScroller/HBoxContainer/StructurePage/Description,
$StageRows/PageScroller/HBoxContainer/LifePage/Description $World/PageScroller/HBoxContainer/LifePage/Description
] ]
@onready var date_time: Label = $StageRows/TopMenu/InfoPanel/DateTime @onready var date_time: Label = $World/TopMenu/InfoPanel/DateTime
@onready var g_speed_icons: Array = [ @onready var g_speed_icons: Array = [
$StageRows/TopMenu/InfoPanel/Speed/Speed1, $World/TopMenu/InfoPanel/Speed/Speed1,
$StageRows/TopMenu/InfoPanel/Speed/Speed2, $World/TopMenu/InfoPanel/Speed/Speed2,
$StageRows/TopMenu/InfoPanel/Speed/Speed3, $World/TopMenu/InfoPanel/Speed/Speed3,
$StageRows/TopMenu/InfoPanel/Speed/Speed4, $World/TopMenu/InfoPanel/Speed/Speed4,
$StageRows/TopMenu/InfoPanel/Speed/Speed5, $World/TopMenu/InfoPanel/Speed/Speed5,
$StageRows/TopMenu/InfoPanel/Speed/Speed6, $World/TopMenu/InfoPanel/Speed/Speed6,
$StageRows/TopMenu/InfoPanel/Speed/Speed7, $World/TopMenu/InfoPanel/Speed/Speed7,
$StageRows/TopMenu/InfoPanel/Speed/Speed8 $World/TopMenu/InfoPanel/Speed/Speed8
] ]
@onready var g_time_minus: TextureButton = $StageRows/TopMenu/TimeMinus @onready var g_time_minus: TextureButton = $World/TopMenu/TimeMinus
@onready var g_time_plus: TextureButton = $StageRows/TopMenu/TimePlus @onready var g_time_plus: TextureButton = $World/TopMenu/TimePlus
@onready var screens: Dictionary = {
"World": $World,
"MainMenu": $MainMenu,
"Report": $Report,
"Achievements": $Achievements,
"Credits": $Credits,
}
var g_tween: Tween = null var g_tween: Tween = null
var g_dragged_start: Vector2 = Vector2.ZERO var g_dragged_start: Vector2 = Vector2.ZERO
@@ -79,60 +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:
var item_id: String = owniverse.entities[_item_id].count_as
if item_id == "":
item_id = _item_id
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:
@@ -161,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()
@@ -172,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)
@@ -207,19 +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.produce_hs(item.item_id, 100_000) var _value = owniverse.state.amount[item.item_id]
_value = E.multiply(9, _value) if E.gt(_value, 0) else 1
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 = []
@@ -227,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
@@ -248,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:
@@ -273,79 +274,130 @@ 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
func _ready() -> void: func _ready() -> void:
var inited_result = init_enitities() get_tree().auto_accept_quit = false
locked_items = inited_result.locked_items world_indicators = WorldIndicators.new(self)
create_new_universe()
load_data() load_data()
_init_owniverse_items() _refresh_universe_ui()
owniverse.refresh_item_amounts()
_change_speed(0)
set_datetime(owniverse.state.time.year)
set_owniverse_item_value()
_unlock_items()
g_counters.swipe.sec = 0.2 g_counters.swipe.sec = 0.2
for pa in page_actions: for pa in page_actions:
page_actions[pa].action_pressed.connect(_page_action_pressed) page_actions[pa].action_pressed.connect(_page_action_pressed)
page_actions[pa].page_id = pa page_actions[pa].page_id = pa
## Starts a fresh universe. Can be connected directly to a button's pressed signal.
## Saving continues through the normal autosave/close path.
func create_new_universe() -> void:
var previous: Owniverse = owniverse
owniverse = Owniverse.new()
var inited_result: Dictionary = init_enitities()
locked_items = inited_result.locked_items
for entity in previous.catalog.entities.values():
entity.free()
previous.free()
if is_instance_valid(g_tween):
g_tween.kill()
g_active_items.clear()
description_targets.clear()
description_samples.clear()
description_rates.clear()
description_elapsed = 0.0
active_actions_dirty = true
active_actions_elapsed = 0.0
for label in g_item_descriptions:
label.text = ""
for item in owniverse_items.values():
item.get_parent().remove_child(item)
item.queue_free()
_init_owniverse_items()
for page in range(5, item_pages.size()):
item_pages[page].visible = false
for actions in page_actions.values():
actions.set_actions("", 0.0)
pending_page = RESOURCE_PAGE
_restore_page.call_deferred()
_refresh_universe_ui()
_on_topmenu_button_pressed("World")
func _refresh_universe_ui() -> void:
owniverse.presentation.refresh(owniverse.state, owniverse.catalog)
_change_speed(0)
set_datetime(owniverse.state.time.year)
set_owniverse_item_value()
_unlock_items()
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
if indicator_elapsed >= 0.1:
indicator_elapsed = 0.0
world_indicators.update(owniverse)
_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:
@@ -363,25 +415,13 @@ 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(_ButtonName) -> void: func _on_topmenu_button_pressed(screen_name: String) -> void:
print("Debug → ")
#, owniverse.state) for screen_id in screens:
for id in owniverse.state.batches: #print(screen_name, " ←→ ", screen_id)
var s = owniverse.state.batches[id].size() screens[screen_id].visible = screen_id == screen_name
if s != 0:
print(id, " → ", s)
#, owniverse.state.batches[id])
if id == 'HG':
print(owniverse.state.batches[id])
#var s: float = 1E15
#var z = 1.0
#for i in range(30):
#print(i, " | ", z, " | ", " | ", s - z)
#z *= 10
print("-------")
func _on_unlock_and_save_timeout() -> void: func _on_unlock_and_save_timeout() -> void:
@@ -409,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
@@ -429,53 +470,65 @@ func save_data() -> void:
var page: int = pending_page var page: int = pending_page
if page < 0: if page < 0:
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.save_data("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},
}) })
var closing: bool = false
func _notification(what: int) -> void: func _notification(what: int) -> void:
if is_node_ready() and (what == NOTIFICATION_WM_CLOSE_REQUEST or what == NOTIFICATION_APPLICATION_PAUSED): if not is_node_ready() or closing:
return
if what == NOTIFICATION_WM_CLOSE_REQUEST:
_close_after_save()
elif what == NOTIFICATION_APPLICATION_PAUSED:
save_data() save_data()
# The OS may suspend frames immediately; finish the queued write now.
SaveManager.flush_saves()
func _close_after_save() -> void:
closing = true
process_mode = Node.PROCESS_MODE_DISABLED
save_data()
if SaveManager.is_saving():
await SaveManager.saves_drained
get_tree().quit()
func init_enitities() -> Dictionary: 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
#endregion #endregion
func _on_achievements_pressed(extra_arg_0: String) -> void:
pass # Replace with function body.
func _on_credits_pressed(extra_arg_0: String) -> void:
pass # Replace with function body.
+121 -4
View File
@@ -1,5 +1,72 @@
# Проверки поэтапного переноса из 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`
Проверяются одновременная разблокировка подробных и общих иконок EON/LP и C0.0/C0, количества свободных и связанных планет без двойного учёта, переключение подпанелей и очистка счётчика при исчезновении стадии. Сохранение игрока не используется.
## Параметры мира и индикаторы
```sh
godot --headless --path Owniverse.GI --script res://tests/test_world_indicators.gd
```
Проверяются сроки жизни существующих и новых звёзд при смене горючести, вероятности и мощность взрывов, влияние агрессии/порядка/просвещения на жизнь, накопление тенденций по времени и этикам, несколько событий за шаг, продолжение после сериализации, валидация сохранения, границы шкал и описания на двух языках. Сцена использует фикстуру без доступа к сохранению игрока.
## Прямой расчёт завершившихся сверхновых
```sh
godot --headless --path Owniverse.GI --script res://tests/test_short_lived_events.gd
```
Сравниваются ускоренный путь и прежний путь с созданием всех партий, отключаемый только в тестовой фикстуре. Проверяются LRG, HRG, RSG и RHG на границах коротких и длинных интервалов, существующие SN, нехватка энергии, ресурсы, бласты, привязки, остатки округления и живые партии. Дополнительно проверяется, что длинный интервал создаёт не более двух живых окон и одну запись завершений, сохраняя общее количество. Сохранение игрока этот тест не использует.
`test_stellar_rules.gd` также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в `../simulation_rules.md`. `test_stellar_rules.gd` также проверяет автоматическое создание HBD: долю 0,1% нового автоматического водорода, появление по одному со случайным округлением, сохранение оплаченного остатка топлива, отсутствие повторного начисления на старый запас, продолжение случайной последовательности после сериализации и переход HBD → CBD с сохранением счётчика BD. Формулы описаны в `../simulation_rules.md`.
## Фоновое сохранение и частота обновления интерфейса ## Фоновое сохранение и частота обновления интерфейса
@@ -13,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 игрового года
@@ -25,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. Прогресс несовместимого файла не переносится, исходные байты сохраняются в архиве. Ошибка архивирования, расшифровки или повреждение данных текущего формата по-прежнему блокируют перезапись.
## Согласованные правила эволюции ## Согласованные правила эволюции
@@ -88,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 отрисовки, а не число завершённых транзакций симуляции в секунду.
+74
View File
@@ -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])
+1
View File
@@ -0,0 +1 @@
uid://b5w4sp6eepypa
+47
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://dji1apht4pn5y
+40
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bboo6op14es24
+56
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://b3gmlo2vq6kk4
+19
View File
@@ -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
View File
@@ -0,0 +1 @@
uid://d1wm7nav71y3o
File diff suppressed because one or more lines are too long
+30
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://bjloakx4qdpsj
File diff suppressed because one or more lines are too long
+59
View File
@@ -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
View File
@@ -0,0 +1 @@
uid://dxsly3qpyqrjy
+5
View File
@@ -0,0 +1,5 @@
extends "res://world/owniverse/owniverse.gd"
# Compare with the materialized-batch path, without overriding the coordinator.
func _init() -> void:
stellar.direct_completions = false
+1
View File
@@ -0,0 +1 @@
uid://cn6hhpjw1sjpb
+22 -21
View File
@@ -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))
+70 -35
View File
@@ -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,93 +9,127 @@ 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
# regression back to the synchronous API in stage.gd.
var shared_manager: Node = root.get_node("SaveManager")
var original_directory: String = shared_manager.save_directory
shared_manager.save_directory = directory
var stage = load("res://stage/stage.gd").new()
stage.pending_page = 2
stage.owniverse.state.energy = 42
stage.save_data()
check(shared_manager.is_saving(), "Stage queues a background save instead of waiting for disk")
stage.owniverse.state.energy = 43
shared_manager.flush_saves()
check(E.eq(shared_manager.load_data("owniverse", "qwerty").state.energy, 42), "Stage save writes an independent snapshot")
stage.owniverse.free()
stage.free()
shared_manager.save_directory = original_directory
global.save_plaintext_copy = old_plaintext global.save_plaintext_copy = old_plaintext
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))
+31 -30
View File
@@ -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))
+93 -92
View File
@@ -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))
+82 -81
View File
@@ -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
View File
@@ -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))
+72
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://8vkudj0tlnng
+101
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://wxhre4pgx1lb
+100
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://te15r63sqhxs
+31 -30
View File
@@ -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
View File
@@ -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))
+69
View File
@@ -0,0 +1,69 @@
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(value: bool, message: String) -> void:
checks = E.add(checks, 1)
if (not value):
failures = E.add(failures, 1)
push_error(message)
func run() -> void:
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
stage.owniverse.free()
stage.set_script(load("res://tests/fixtures/ui_stage.gd"))
root.add_child(scene)
await process_frame
await process_frame
var world = stage.owniverse
world.state.binds.RD = {"EON1": 10, "EON2": 4, "C0.0": 3}
world.state.amount.EON1 = 2
world.state.stats.total_amount.EON1 = 12
world.state.stats.total_amount.EON2 = 4
world.state.stats.total_amount["C0.0"] = 3
var before: Dictionary = world.state.duplicate(true)
stage._refresh_universe_ui()
var counts: Dictionary = world.presentation.amounts(world.state, world.catalog)
check(((E.eq(counts.EON1, 12) and E.eq(counts.EON2, 4)) and E.eq(counts.LP, 16)), "Individual eons and their total include bound planets")
check(((E.eq(counts["C0.0"], 3) and E.eq(counts.C0, 3)) and E.eq(counts.Planet, 19)), "Civilization detail does not double the planetary total")
check((E.eq(world.presentation.amounts(world.state, world.catalog, true).EON1, 12) and E.eq(world.presentation.amounts(world.state, world.catalog, true).LP, 16)), "Historical counts retain individual and aggregate values")
check(E.eq(world.state, before), "Presentation does not modify simulation state")
var pending: Array = ["EON1", "EON2", "EON3", "C0.0"]
var pending_before: Array = pending.duplicate()
var decision: Dictionary = world.presentation.unlocks(world.state, world.catalog, pending)
check(E.eq(decision.remaining, ["EON3"]), "Only unmet requirements remain pending")
for id in ["EON1", "EON2", "LP", "C0.0", "C0"]:
check(decision.visible.has(id), E.add("Unlock projection includes detail and alias: ", id))
check((E.eq(pending, pending_before) and E.eq(world.state, before)), "Unlock evaluation leaves inputs untouched")
var snapshot: Dictionary = world.presentation.pending_amounts.duplicate()
world.presentation.deferred_updates = true
world.state.amount.EON1 = 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)
check(stage.owniverse_items.EON1.is_visible_in_tree(), "Selecting LP shows its unlocked eons")
stage.item_activated(stage.owniverse_items.C0)
check((stage.owniverse_items["C0.0"].is_visible_in_tree() and (not stage.owniverse_items.EON1.is_visible_in_tree())), "Selecting C0 switches the detail panel")
world.state.binds.RD.EON2 = 0
stage._refresh_universe_ui()
check((stage.owniverse_items.EON2.visible and E.eq(stage.owniverse_items.EON2.description.text, "")), "Previously reached stage stays unlocked after extinction with no stale count")
for entity in world.catalog.entities.values(): entity.free()
world.free()
scene.free()
print(("Life icons: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+1
View File
@@ -0,0 +1 @@
uid://c4p1ad0ij32xo
+39
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://d2xk4ldldsgo3
+45 -31
View File
@@ -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))
+112
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bvri1enpsjisk
+110
View File
@@ -0,0 +1,110 @@
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 fixture(legacy: bool):
var world = load(("res://tests/fixtures/legacy_stellar_events.gd" if legacy else "res://world/owniverse/owniverse.gd")).new()
for id in root.get_node("Global").OwniverseEntities:
world.catalog.entities[id] = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
world.state.amount[id] = 0
world.state.stats.total_amount[id] = 0
if E.ne(world.catalog.entities[id].group_as, ""):
world.state.amount[world.catalog.entities[id].group_as] = 0
world.state.stats.total_amount[world.catalog.entities[id].group_as] = 0
return world
func dispose(world) -> void:
for entity in world.catalog.entities.values(): entity.free()
world.free()
func equivalent(a: Variant, b: Variant) -> bool:
if ((a is Dictionary) and (b is Dictionary)):
if E.ne(a.size(), b.size()): return false
for key in a:
if ((not b.has(key)) or (not equivalent(a[key], b[key]))): return false
return true
if ((a is Array) and (b is Array)):
if E.ne(a.size(), b.size()): return false
for i in a.size():
if (not equivalent(a[i], b[i])): return false
return true
if E.numeric(a) and E.numeric(b):
# Counts must match exactly. Fractional float remainders can differ
# 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:
for parent in ["LRG", "HRG", "RSG", "RHG"]:
for year in [101, E.of("101.5"), E.of("104.75"), E.of("1767.3"), 3005]:
var fast = fixture(false)
var legacy = fixture(true)
fast.state.amount[parent] = 20000
fast.state.amount.RD = 100000
fast.state.amount.HP = 50
fast.state.amount.Met = 100
fast.state.binds.RD = {"HP": 2000, "C0.1": 100}
fast.state.binds[parent] = {"HP": 10}
fast.state.batches[parent] = [{"amount": 20000, "start_year": E.of("100.3"), "end_year": E.of("3000.7")}]
# Existing short-lived populations test integration with new arrivals.
fast.state.amount.SN = 3
fast.batches.append_batch(fast.state, "SN", 3, E.of("100.5"), E.of("100.5"))
if E.eq(year, E.of("104.75")): fast.state.energy = 7
legacy.state = fast.state.duplicate(true)
fast.evolution.evolve_batches(fast.state, year)
legacy.evolution.evolve_batches(legacy.state, year)
for field in fast.state:
check(equivalent(fast.state[field], legacy.state[field]), ("%s at %s: %s matches materialized events" % [parent, year, field]))
for id in ["SN", "DSN", "BSN", "HN", "GRB", "SGRB", "LGRB", "HGRB"]:
check(E.le(fast.state.batches.get(id, []).size(), 3), "Only surviving event windows remain")
dispose(fast)
dispose(legacy)
var fast = fixture(false)
var pending: Dictionary = {}
fast.batches.append_batch(fast.state, "SN", 10000, E.of("0.25"), E.of("1667.25"), pending, E.of("1667.25"))
check((E.le(fast.state.batches.SN.size(), 2) and E.eq(pending.SN.size(), 1)), "Completed windows aggregate without transient batch allocation")
var total: Variant = pending.SN[0].amount
for batch in fast.state.batches.SN: total = E.add(total, batch.amount)
check(E.eq(total, 10000), "Direct completions plus survivors conserve event count")
dispose(fast)
# Dense completed events must not iterate a million elapsed windows.
# 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))
+1
View File
@@ -0,0 +1 @@
uid://dp7a88n0mmyb5
+52
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://gtvie7p6j62x
+168 -156
View File
@@ -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,236 +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()
var rd = world.entities.RD for id in ["DSN", "SN", "BSN", "HN", "SGRB", "GRB", "LGRB", "HGRB"]:
check(rd.planets.HP.chance == 0.01, "Preserve fractional planet probability") 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))
check(rd.planets.GG.amount == 2.0 and rd.planets.GG.chance == 0.08, "Read count and chance separately") world.catalog.entities.SN.output_and.blast = 999
check(rd._parse_planet_string("HP:1:0.01") == {"HP": {"amount": 1.0, "chance": 0.01}}, "Accept a single planet term") world.catalog.entities.SN.blast = 4
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).blast, 8), "Imported output cannot override or double blast power")
world.catalog.entities.SN.blast = 0
check((not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).has("blast")), "Zero entity blast disables external event")
world.catalog.entities.SN.output_or = {"positive": "blast:999", "negative": "blast:999", "chance": 50}
check((not world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).has("blast")), "Alternative output cannot restore disabled blast")
dispose(world)
world = fixture()
var rd = world.catalog.entities.RD
check(E.eq(rd.planets.HP.chance, E.of("0.01")), "Preserve fractional planet probability")
check((E.eq(rd.planets.GG.amount, 2) and E.eq(rd.planets.GG.chance, E.of("0.08"))), "Read count and chance separately")
check(E.eq(rd._parse_planet_string("HP:1:0.01"), {"HP": {"amount": 1, "chance": E.of("0.01")}}), "Accept a single planet term")
check(rd._parse_planet_string("").is_empty(), "Empty planet definitions stay empty") 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
View File
@@ -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
View File
@@ -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))
+189
View File
@@ -0,0 +1,189 @@
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 Descriptions = preload("res://world/owniverse/item_description.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 fixture():
var world = load("res://world/owniverse/owniverse.gd").new()
for id in root.get_node("Global").OwniverseEntities:
var entity = load("res://world/owniverse/owniverse_entity.gd").new(root.get_node("Global").OwniverseEntities[id])
world.catalog.entities[id] = entity
world.state.amount[id] = 0
world.state.stats.total_amount[id] = 0
if E.ne(entity.group_as, ""):
world.state.amount[entity.group_as] = 0
world.state.stats.total_amount[entity.group_as] = 0
return world
func dispose(world) -> void:
for entity in world.catalog.entities.values(): entity.free()
world.free()
func run() -> void:
var global: Node = root.get_node("Global")
var world = fixture()
world.state.amount.RD = 400
world.batches.append_batch(world.state, "RD", 400, 0, 0)
var end: Variant = world.state.batches.RD[0].end_year
world.environment.modify_world_parameter("Bur", 9)
check(E.near(world.state.batches.RD[0].end_year, E.divide(end, 10)), "Burnability accelerates existing cohorts")
var count: Variant = world.state.batches.RD[0].amount
world.environment.modify_world_parameter("Bur", E.neg(9))
check((E.near(world.state.batches.RD[0].end_year, end) and E.eq(world.state.batches.RD[0].amount, count)), "Changing burnability back preserves population and lifetime")
world.environment.modify_world_parameter("Bur", 9)
world.batches.append_batch(world.state, "YD", 10, 0, 0)
check(E.near(world.state.batches.YD[0].start_year, E.divide(world.catalog.entities.YD.lifespan, 10)), "New stars use the same burnability")
world.state.time = {"datetime": E.subtract(E.divide(end, 10), E.of("0.1")), "year": E.subtract(E.divide(end, 10), E.of("0.1"))}
world.batches.withdraw_batches(world.state, "RD", world.state.time.year)
world.environment.modify_world_parameter("Bur", E.neg(18))
check(E.ge(world.state.batches.RD[0].end_year, world.state.time.year), "Slowing a partially evolved cohort keeps its remaining window in the future")
world.environment.modify_world_parameter("Exp", 9)
check(E.eq(world.stellar.evolution_products(world.state, world.catalog.entities.SN, 2).blast, 180), "Explosion power includes world explosiveness")
var products: Dictionary = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 1000)
check((E.eq(products.get("GRB", 0), 100) and E.eq(products.get("SN", 0), 900)), "Explosiveness raises rare-event probability without duplicating stars")
world.catalog.entities.HRG.output_or.chance = 50
products = world.stellar.evolution_products(world.state, world.catalog.entities.HRG, 100)
check((E.eq(products.get("GRB", 0), 100) and E.eq(products.get("SN", 0), 0)), "Rare-event probability is bounded at one")
dispose(world)
world = fixture()
var ordinary: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
world.state.life_remainders.clear()
world.environment.modify_world_parameter("Agg", 9)
var aggressive: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
check(E.gt(aggressive.EON1, ordinary.EON1), "Aggression accelerates primitive evolution")
world.state.life_remainders.clear()
world.state.world_parameters.Order = 10
var orderly: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
check(E.lt(orderly.RP, aggressive.RP), "Order reduces degradation")
world.state.life_remainders.clear()
world.state.world_parameters.Enlightenment = 10
var enlightened: Dictionary = world.life.life_products(world.state, world.catalog.entities.HP, 1000000)
check(E.gt(enlightened.EON1, orderly.EON1), "Enlightenment increases primitive progress")
dispose(world)
world = fixture()
var hotter = fixture()
for simulation in [world, hotter]:
simulation.state.amount.RD = 1000000
simulation.batches.append_batch(simulation.state, "RD", 1000000, 0, 0)
hotter.environment.modify_world_parameter("Bur", 9)
world.evolute(1000)
hotter.evolute(1000)
check(E.gt(hotter.state.amount.S, world.state.amount.S), "Burnability changes actual space production")
check(E.eq(E.add(hotter.state.energy, hotter.state.amount.S), Parameters.INITIAL_ENERGY), "Enhanced production still spends the GI energy budget")
dispose(world)
dispose(hotter)
world = fixture()
world.state.amount["C0.2"] = 1000000
world.state.ethics["C0.2"] = {"m": 1000000}
var baseline: Dictionary = world.state.duplicate(true)
var advanced: Dictionary = baseline.duplicate(true)
advanced.world_parameters.Agg = 19
var next_stage: String = global.CivilizationRules["C0.2"].next
var outcomes: Array = []
for target in [baseline, advanced]:
var expired: Dictionary = {"C0.2": [{"amount": 1000000, "from": 0, "to": 1}]}
var budget: Dictionary = CivilizationState.stellar_budget(world, target, global.CivilizationRules)
CivilizationEvolution.prepare(world, target, expired, 1, budget, global.CivilizationRules)
outcomes.append(expired["C0.2"][0].products.get(next_stage, 0))
check(E.gt(outcomes[1], outcomes[0]), "Aggression changes actual ethical civilization transitions")
dispose(world)
# Trends use ethical proportions and stage weights, not population magnitude.
world = fixture()
var rules: Dictionary = {"C2.0": {"home": "DwStar"}, "C2.1": {"home": "DwStar"}}
world.state.ethics = {"C2.0": {"d": 100}, "C2.1": {"d": 100}}
var source: Dictionary = world.state.duplicate(true)
var single: Dictionary = source.duplicate(true)
var split: Dictionary = source.duplicate(true)
Parameters.advance(source, single, rules, 1000000)
for i in 10: Parameters.advance(source, split, rules, 100000)
check(E.near(single.trends.Integration, E.of("0.018")), "Trend integrates elapsed years and doubled stage weights")
for key in Parameters.TRENDS:
check(E.near(single.trends[key], split.trends[key]), E.add("Trend is independent of frame partition: ", key))
source.ethics["C2.0"].d = 1000000
var larger: Dictionary = world.state.duplicate(true)
Parameters.advance(source, larger, rules, 1000000)
check(E.near(larger.trends.Integration, single.trends.Integration), "Population proportions determine trend influence")
world.state.trends.Integration = E.of("0.99")
Parameters.advance(source, world.state, rules, 100000000)
check(E.eq(world.state.civilization_stats["trend:Integration"], 2), "Long steps process every crossed threshold")
check(E.near(world.state.trends.Integration, E.of("0.79")), "Threshold crossing preserves the fractional remainder")
check(((E.gt(world.state.world_parameters.Federation, 1) or E.gt(world.state.world_parameters.Enlightenment, 1)) or E.gt(world.state.world_parameters.Order, 1)), "Integration events change the world")
var restored: Dictionary = E.decode(bytes_to_var(var_to_bytes(E.encode(world.state))))
Parameters.advance(source, world.state, rules, 100000000)
Parameters.advance(source, restored, rules, 100000000)
check(E.eq(world.state, restored), "Trend events resume identically after serialization")
Parameters.advance(source, world.state, rules, E.of("1.0e12"))
check((E.ge(world.state.trends.Integration, 0) and E.lt(world.state.trends.Integration, 1)), "Very large intervals finish with a valid trend fraction")
check(E.ge(world.state.world_parameters.Agg, 1), "Repeated integration events cannot reduce aggression below the scale")
var payload: Dictionary = {"format_version": 5, "app_version": "test", "ui": {"current_page": 1}, "state": world.state}
var manager: Node = root.get_node("SaveManager")
check(E.eq(manager.validate_data(payload), ""), "New parameters and trends pass save validation")
var old: Dictionary = payload.duplicate(true)
old.state.erase("world_parameters")
old.state.erase("trends")
check(E.eq(manager.validate_data(old), ""), "Existing current-format saves remain readable")
payload.state.world_parameters.Agg = 20
check(E.ne(manager.validate_data(payload), ""), "Out-of-range scales are rejected")
dispose(world)
for sample in [[E.of("1e-7"), 0], [E.of("1e-6"), E.of("0.5")], [E.of("1e-5"), 1], [E.of("0.9"), 0], [E.of("0.95"), E.of("0.5")], [1, 1]]:
check(E.near(Parameters.density_display(sample[0]).position, sample[1]), E.add("Density boundary ", str(sample[0])))
# The fixture overrides saving/loading; the player's save is never accessed.
var scene = load("res://world/world.tscn").instantiate()
var stage = scene.get_node("Stage")
stage.owniverse.free()
stage.set_script(load("res://tests/fixtures/ui_stage.gd"))
root.add_child(scene)
await process_frame
await process_frame
stage.owniverse.state.energy = E.divide(Parameters.INITIAL_ENERGY, 4)
stage.owniverse.state.world_parameters.Bur = 19
stage.owniverse.state.trends.Integration = E.of("0.75")
stage.world_indicators.update(stage.owniverse)
var warning: TextureRect = scene.get_node("Background/WarningBorder")
check(E.eq(warning.mouse_filter, Control.MOUSE_FILTER_IGNORE), "Warning overlay does not block input")
var environments: Array = [[E.of("1.0e-7"), "ff0000ff"], [E.of("1.0e-6"), "df2020bf"], [E.of("1.0e-5"), "c040407f"], [E.of("0.1"), "00000000"], [E.of("0.9"), "40a0c07f"], [E.of("0.97"), "20b0dfbf"], [1, "00c0ffff"], [E.of("0.1"), "00000000"]]
for sample in environments:
check(Parameters.density_warning_color(sample[0]).is_equal_approx(Color.html(sample[1])), E.add("Unity warning threshold: ", str(sample[0])))
stage.owniverse.state.amount.H = E.of("1e14")
stage.world_indicators.update(stage.owniverse)
check(warning.self_modulate.is_equal_approx(Color.html("00c0ffff")), "Compression warning uses current universe density")
stage.owniverse.state.amount.H = 0
stage.owniverse.state.amount.S = E.of("1e24")
stage.world_indicators.update(stage.owniverse)
check(warning.self_modulate.is_equal_approx(Color.html("ff0000ff")), "Rip warning replaces compression warning")
stage.owniverse.state.amount.S = 0
stage.world_indicators.update(stage.owniverse)
check(E.eq(warning.self_modulate.a, 0), "Warning clears when density returns to normal")
for bar in stage.world_indicators.energy_bars:
check(E.eq(bar.scale.x, E.of("0.25")), "Energy fill is synchronized on both pages")
var pointer: Control = stage.world_indicators.pointers.Bur
check(E.near(E.add(pointer.position.x, E.divide(pointer.size.x, 2)), 304), "Burnability pointer reaches the correct scale endpoint")
check(E.eq(stage.world_indicators.trend_bars[1].scale.y, E.of("0.75")), "Trend bar displays its stored level")
for locale in ["en", "ru"]:
global.Locale = locale
for id in ["Energy", "Density", "Burnability", "Explosiveness", "Aggression", "Trends"]:
var description: String = Descriptions.render(stage.owniverse, global, id, "Indicator")
check(((not description.contains("—")) and (not description.contains("%value"))), E.add(E.add(E.add("Resolved indicator description: ", locale), "/"), id))
dispose(stage.owniverse)
scene.free()
print(("World indicators: %d checks, %d failures" % [checks, failures]))
quit((0 if E.eq(failures, 0) else 1))
+1
View File
@@ -0,0 +1 @@
uid://dmldg06dx3utl
Binary file not shown.
+200
View File
@@ -0,0 +1,200 @@
|a1|1|age::1E9|age::1E9|A good start!|The Universe exists for a billion years.
|a2|3|age::1E10|age::1E10|Middle age.|The Universe exists for a ten billions years.
|a3|6|age::1E11|age::1E11|Impressive term.|The Universe exists for a hundred billions years.
|a4|10|age::1E12|age::1E12|Isn't it too long?|The Universe exists for a trillion years.
||1||uptime|The universe is under your control for 13 minutes in a row.|Not bad for a start!
||3|||The universe is under your control for 34 minutes in a row.|It would be worse without you.
||6|||The universe is under your control for 1 hour 29 minutes in a row.|Don't draw unnecessary attention.
||10|||The universe is under your control for 3 hours 53 minutes in a row!|You can also relax, this is the final achievement in this line!
||1||played|You rule the universe for 8 hours.|Its inhabitants should be grateful to you.
||3|||You rule the universe for 21 hours.|Surely a lot has become clear by now.
||6|||You rule the universe for 2 days and 7 hours.|Tell your friends about the game!
||10|||You rule the universe for 6 days.|Share your experience on social networks!
|a25|1||total:H:1000|Get total 1 000 hydrogen|
|a26|1|total:H:5E4|total:H:1E6|Get total 1 million hydrogen|
|a27|1|total:H:5E7|total:H:1E9|Get total 1 billion hydrogen|
|a28|3|total:H:5E10|total:H:1E12|Get total 1 trillion hydrogen|
|a29|3|total:H:5E13|total:H:1E15|Get total 1 quadrillion hydrogen|
|a30|3|total:H:5E16|total:H:1E18|Get total 1 quintillion hydrogen|
|a31|6|total:H:5E19|total:H:1E21|Get total 1 sextillion hydrogen|
|a32|6|total:H:5E22|total:H:1E24|Get total 1 septillion hydrogen|
||1||total:S:1000|Create 1 000 cubic parsecs of space|
||1|total:S:5E4|total:S:1E6|Create 1 million cubic parsecs of space|
||1|total:S:5E7|total:S:1E9|Create 1 billion cubic parsecs of space|
||3|total:S:5E10|total:S:1E12|Create 1 trillion cubic parsecs of space|
||3|total:S:5E13|total:S:1E15|Create 1 quadrillion cubic parsecs of space|
||3|total:S:5E16|total:S:1E18|Create 1 quintillion cubic parsecs of space|
||6|total:S:5E19|total:S:1E21|Create 1 sextillion cubic parsecs of space|
||6|total:S:5E22|total:S:1E24|Create 1 septillion cubic parsecs of space|
||1|total:He:1000|total:He:1000|Obtain 1 billion helium|
||3|total:He:1E12|total:He:1E12|Obtain 1 quadrillion helium|
||6|total:He:1E21|total:He:1E21|Obtain 1 sextilion helium|
||1|||Use up 1 million helium|
||3|||Use up 1 trillion helium|
||6|||Use up 1 quintillion helium|
||1|total:Voids:5|total:Voids:25|Form 25 voids|
||1|total:Voids:25|total:Voids:125|Form 125 voids|
||1|total:Voids:125|total:Voids:625|Form 625 voids|
||3|total:Voids:625|total:Voids:3125|Form 3125 voids|
||3|total:Voids:3125|total:Voids:15625|Form 15625 voids|
||6|total:Voids:15625|total:Voids:78125|Form 78125 voids|Keep up the good work, without voids the universe is doomed!
|a33|10|energy::1E27|energy::1E27|Out of ammo!|All energy cache is spent.
|a5|1||total:Star:1|Make a Star|
|a6|1|total:OD:1|total:OD:1|Make an Orange Dwarf|
|a7|1||total:YD:1|Make a Yellow Dwarf|
|a8|1|total:WYD:1|total:WYD:1|Make a White-Yellow Dwarf|
|a9|1|total:YG:1|total:YG:1|Make a Yellow Giant|
|a10|1||total:BG:1|Make a Blue Giant|
|a11|1|total:BG:1|total:SG:1|Make a Supergiant|
|a12|1|total:SG:1|total:HG:1|Make a Hypergiant|
|a13|1||total:WD:1|Get a White Dwarf|
|a14|1||total:NS:1|Get a Neutron Star|
|a15|1|total:NS:1|total:MGT:1|Get a Magnetar|
|a16|1||total:BH:1|Get a Black Hole|
|a17|1|total:BH:1|total:SMBH:1|Make a Supermassive Black Hole|
|a18|1||total:N:1|Make a Nebula|
|a19|1|total:N:1|total:G:1|Make a Globula|
|a20|1|total:G:1|total:STN:1|Make a Star Nursery|
|a21|1|total:G:1|total:MC:1|Make a Molecular Cloud|
|a22|3|total:STN:1|total:ASTN:1|Make an Active Star Nursery|
|a23|3|total:MC:100|total:MC:1E4|Make 10 000 Molecular Clouds|
|a34|1|auto:Star:1|auto:Star:10|Make 10 stars via clouds|
|a35|1|auto:Star:10|auto:Star:1000|Make 1 000 stars via clouds|
|a36|3|auto:Star:1000|auto:Star:1E6|Make 1 million stars via clouds|
|a37|1|total:Star:30|total:Star:300|Make 300 stars|
|a38|1|total:OD:1|total:OD:2000|Make 2 000 orange dwarfs|
|a39|1|total:WYD:1|total:WYD:500|Make 500 white-yellow dwarfs|
|a40|1|total:BD:1|total:BD:1E6|Make 1 million brown dwarfs|
|a41|3|total:HG:1000|total:HG:1000|Someone must need them, of course?|Make 1 000 hypergiants.
|a42|3|total:Star:5000|total:Star:1E7|Make 10 million stars|
||3|total:Star:4E9|total:Star:4E11|Make 400 billion stars|
||3|total:Star:9E12|total:Star:9E14|Make 90 quadrillion stars|
|a43|3|total:SMBH:1|total:SMBH:1E4|Drop 10 000 matter into Supermassive Black Holes|
||3|total:SMBH:3E5|total:SMBH:4.2E7|Drop 42 million matter into Supermassive Black Holes|Sagittarius A* weighs about as much.
||3|total:SMBH:3E9|total:SMBH:6.6E11|Drop 660 billion matter into Supermassive Black Holes|TON 618 weighs about as much.
|a44|1||total:SC:1|Make a Star Cluster|
|a45|1||total:SCM:1|Make a Medium Star Cluster|
|a46|1|total:SC:1|total:SCL:1|Make a Large Star Cluster|
|a47|3|total:SCL:1|total:SCG:1|Make a Giant Star Cluster|
|a48|3|total:SC:10|total:SC:100|Make 100 Star Clusters|
|a49|1||total:GAL:1|Make a Galaxy|
|a50|3|total:GAL:1|total:GALS:1|Make a Spiral Galaxy|
|a51|1||total:GEM:1|Make a Medium Galaxy|
|a52|1|total:GAL:1|total:GEL:1|Make a Large Galaxy|
|a53|3|total:GEL:1|total:GEG:1|Make a Giant Galaxy|
|a54|3|total:GAL:5|total:GAL:30|Make 30 Galaxies|
|a55|1|total:GAL:30|total:LGG:1|Make a Galactic Group|
|a56|3|total:LGG:1|total:LGG:30|Make 30 Galactic Groups|
|a57|3|total:LGG:30|total:LSC:1|Make a Galactic Supercluster|
|a58|3|total:LSC:1|total:LSC:30|Make 30 Galactic Superclusters|
|a59|6|total:LSC:30|total:LGF:1|Make a Galactic Filament|
|a60|6|total:LGF:1|total:LGF:100|Make 100 Galactic Filaments|
|a61|6|total:LGF:100|total:LGW:1|Make a Great Wall|
|a62|6|total:LGW:1|total:LGW:100|Make 100 Great Walls|
|a63|6|total:LGW:100|total:LUB:1|Make an Universe Bubble|
|a64|10|total:LUB:1|total:LUB:1|It had to end sometime!|The large-scale structure of the universe has been created.
1|a65|1||Planet:1|Make a planet|
1|a66|1|GG:1^AB:1|RP:1|Make a rocky planet|
1|a67|1|RP:1|HP:1|Make a habitable planet|
1|a68|3||EON1:1|Make life|
1|a69|1|EON1:1|EON2:1|Grow amoebas and algae|
1|a70|1|EON2:1|EON3:1|Grow trilobites and fishes|
1|a71|1|EON3:1|EON4:1|Grow dinosaurs and birds|
1|a72|1|EON4:1|EON5:1|Grow mammals and prepare yourself...|
1|a73|1|Planet:1|Planet:1E5|Make 100 000 planets|
1|a74|1|HP:1|HP:1E4|Make 10 000 habitable planets|
1|a75|3|EON1:1|EON1:1000|Make life at 1 000 planets|
1|a76|6|C0.0:1|C0.0:1|Sapience emerged!|
1|a77|1|C0.0:1|C0.5:1|They can make pots out of clay!|A civilization has reached 0.5 level.
1|a78|3|C0.7:1|C1.0:1|They have mastered their home planet!|A civilization has reached 1.0 level.
1|a79|1|C1.3:1|C1.5:1|They have built a space city!|A civilization has reached 1.5 level.
1|a80|6|C1.7:1|C2.0:1|They have a Dyson Swarm!|A civilization has reached 2.0 level.
1|a81|1|C2.2:1|C2.2:1|They seem to grow at last!|A civilization has settled over a hundred of stars.
1|a82|1|C2.5:1|C2.5:1|They are almost noticeable by a naked eye!|A civilization has settled over a Medium Star Cluster.
1|a83|3|C2.8:1|C2.8:1|Oh, they grasped how to control gravity at last!|A civilization has reached 2.8 level.
1|a84|6|C3.0:1|C3.0:1|They really have learned to fly!|A civilization is able to travel between Galaxies.
1|a85|3|C3.2:1|C3.2:1|They must be so proud of themselves!|A civilization has settled on a scale of an Andromeda Galaxy.
1|a86|3|C3.5:1|C3.5:1|Hey, they are resourceful!|A civilization has utilized a Neutron Star resources.
1|a87|3|C3.8:1|C3.8:1|Now they actually easy to notice.|A civilization has settled over hundreds of thousands Galaxies.
1|a88|6|C4.0:1|C4.0:1|Feels like this has happened already, no?|A civilization has grasp on temporal manipulations.
1|a89|6|C4.2:1|C4.2:1|At last they seem to think big enough.|A civilization has utilized a Supermassive Black Hole for their goals.
1|a90|10|C4.4:1|C4.4:1|They've made it! What will they do next, I wonder?|A civilization has expanded on a scale of a whole visible Universe.
1|a91|3|C1.6:1|SET:1|They have made an extrasolar colony!|
1|a92|6||GSM:1&C2.8:50|They seem to have found a Galactic Federation or something!|
1|a93|1|||Oh no...|A civilization of 0.8+ level extinct.
1|a94|1|||Not again!|100 civilizations of 1.2+ level extinct.
1|a95|1|||Will I get used to it?|1 000 civilizations of 1.6+ level extinct.
1|a96|3|||Yeah, you become numb after a while.|10 000 civilizations of 2.0+ level extinct.
1|a97|6|||They seemed to be survivors. Oh well.|A civilization of 3.0+ level extinct.
1|a98|10|||My dissapointment is immeasureable.|A civilization of 4.0+ level extinct.
1|a99|6|||This reminds me of something...|At the date of 13.8 billions of years there is a civilization of 0.9 level.
1|a102|3|высокая плотность|BigCrunch|Big Crunch!|The universe collapsed into a colossal black hole.
1|a103|3|низкая плотность|BigRip|Big Rip!|The universe dissipated in different directions.
1|a104|6|BigCrunch^BigRip|BigCrunch&BigRip|Double Trouble!|Two different apocalypses are finished.
1||10|||Complete banishment! You have been kicked out of the universe.|Now this universe exists by itself, without your participation.
1||3|||Antitheism: They deny your existence.|Maybe they are somehow right about that?
1||3|||Integration: They strive for coexistence.|Maybe at least they can make it?
1||3|||Xenocide: Their bellicosity leads to dire results.|However, nothing is new.
1||3|||Reconstruction: They rearrange the universe.|It is their universe, right? Or not?
1||3|||Spirituality: They seek deep meaning.|What if they find out the truth?
1||3|||Exodus: They want to leave the universe.|What do they not like here?
1||6|||Dynamic Universe|All trends have shown themselves!
1||3|||Banishment: They are trying to expel you from the universe!|They definitely don't like something.
1||3|||Federation: They are looking for ways to peacefully coexist.|Perhaps this is good?
1||3|||Enlightenment: They help those who are lagging behind.|How lovely!
1||3|||New Order: They bury the hatchet.|Maybe it's for the best?
1||3|||Grand Wars: They are engaged into carnage of epic proportions.|It is a pity that it is impossible to discern all the details from here.
1||3|||Extermination: They are purging potential competitors.|Life is a renewable resource.
1||3|||Devastation: They are destroying the universe!|Stars turn into black holes.
1||3|||Stellar Fragmentation: They reconstruct the universe for themselves.|It is more convenient for them to live on small and stable stars.
1||3|||Stellar Pumping: They create new stars!|Red dwarfs are created from clouds and brown dwarfs.
1||3|||Stellar Cooling: They stabilize the stars.|The activity of stellar processes is decreasing.
1||3|||Prayer: They pray incredibly fervently.|You were even able to hear something.
1||3|||Dialogue: They are waiting for an answer!|And it is you who should answer.
1||3|||Living Universe|10 civilization actions have taken place.
1||6|||Very Living Universe|30 civilization actions have taken place.
1||10|||Incredibly Alive Universe!|100 civilization actions have taken place.
1||10|||So much is happening!|All actions happened at least once!
||1|сила получена|сила применена|Learn to dig fast.|Use Shovelin
||1|сила получена|сила применена|Kickstart inflation.|Use Inflatin
||3|сила получена|сила применена|Pass time.|Use Temporite
||3|сила получена|сила применена|Dispose of garbage.|Use Collisite
||6|сила получена|сила применена|Divide physics by zero.|Use Primex
||1|сила получена|сила применена|Destroy space.|Use Singularite
||1|сила получена|сила применена|Strengthen structures.|Use Renewin
||3|сила получена|сила применена|Light up the stars.|Use Infusite
||3|сила получена|сила применена|Cool down the universe.|Use Frostin
||6|сила получена|сила применена|Get rid of the stars.|Use Destellarite
||1|сила получена|сила применена|Make more planets.|Use Rockex
||1|сила получена|сила применена|Seed the universe.|Use Panspermin
||3|сила получена|сила применена|Fertilize the universe.|Use Gaianite
||3|сила получена|сила применена|Invigorate the universe.|Use Darwinite
||6|сила получена|сила применена|Fill the universe with goodness.|Use Edenex
||1|сила получена|сила применена|Teach sapients to live in peace.|Use Pacifin
||1|сила получена|сила применена|Teach sapients to fight for their lives.|Use Enragein
||3|сила получена|сила применена|Appoint prophets among sapients.|Use Moralite
||3|сила получена|сила применена|Punish those sapients who do not follow your plans.|Use Discriminite
||6|сила получена|сила применена|Suppress those sapients who think too much of themselves.|Use Reprex
||6|одна любая сила получена в 4 градациях|флэш|Use the forces of the one kind by increase of power.|Green, blue, purple, orange
||3|снижена вручную|Bur=1|Cold universe.|Minimal Burnability
||3|аналогично|Bur=19|The stars are lit to burn out!|Maximum Burnability
||3|аналогично|Exp=1|Dense vacuum.|Minimal Explosiveness
||3|аналогично|Exp=19|Bada-bing, bada-boom-boom-boom!|Maximum Explosiveness
||3|аналогично|Agg=1|Peace and tranquility.|Minimal Aggression
||3|аналогично|Agg=19|Like 4E4!|Maximum Aggression
1|a101|6|||All forces are available at once!|
1|a100|10|||All forces are used at least once in this universe.|
||1|3 обычные силы одного вида||Upgrade a force to rare.|
||3|3 редкие силы одного вида||Upgrade a force to epic.|
||6|3 эпические силы одного вида||Upgrade a force to legendary.|
||1|улучш 3 силы|5|Upgrade 5 forces.|
||1|8|13|Upgrade 13 forces.|
||3|21|34|Upgrade 34 forces.|
||3|55|89|Upgrade 89 forces.|
||6|144|233|Upgrade 233 forces.|
||6|377|610|Upgrade 610 forces.|
||10|987|1597|Upgrade 1 597 forces.|Continue in the same spirit!
||1|1|3|Upgrade 3 force chests.|
||3|5|7|Upgrade 7 force chests.|
||6|14|20|Upgrade 20 force chests.|
||10|38|55|Upgrade 55 force chests.|Thanks for support!
Binary file not shown.
+200
View File
@@ -0,0 +1,200 @@
|a1|1|age::1E9|age::1E9|Хорошее начало!|Вселенная существует миллиард лет.
|a2|3|age::1E10|age::1E10|Средний возраст.|Вселенная существует десять миллиардов лет.
|a3|6|age::1E11|age::1E11|Внушительный срок.|Вселенная существует сто миллиардов лет.
|a4|10|age::1E12|age::1E12|А не слишком ли это долго?|Вселенная существует триллион лет
||1||uptime|Вселенная под вашим управлением 13 минут подряд.|Неплохо для начала!
||3|||Вселенная под вашим управлением 34 минуты подряд.|Без вас было бы хуже.
||6|||Вселенная под вашим управлением 1 час 29 минут подряд.|Не привлекайте ненужного внимания.
||10|||Вселенная под вашим управлением 3 часа 53 минуты подряд!|Можно и отдохнуть, это финальное достижение в данной линейке!
||1||played|Вы управляете вселенной 8 часов.|Её обитатели должны быть вам благодарны.
||3|||Вы управляете вселенной 21 час.|Наверняка уже многое стало понятно.
||6|||Вы управляете вселенной 2 суток и 7 часов.|Расскажите об игре друзьям!
||10|||Вы управляете вселенной 6 суток.|Поделитесь опытом в социальных сетях!
|a25|1||total:H:1000|Получи всего 1 000 водорода|
|a26|1|total:H:5E4|total:H:1E6|Получи всего 1 миллион водорода|
|a27|1|total:H:5E7|total:H:1E9|Получи всего 1 миллиард водорода|
|a28|3|total:H:5E10|total:H:1E12|Получи всего 1 триллион водорода|
|a29|3|total:H:5E13|total:H:1E15|Получи всего 1 квадриллион водорода|
|a30|3|total:H:5E16|total:H:1E18|Получи всего 1 квинтиллион водорода|
|a31|6|total:H:5E19|total:H:1E21|Получи всего 1 секстиллион водорода|
|a32|6|total:H:5E22|total:H:1E24|Получи всего 1 септиллион водорода|
||1||total:S:1000|Создай 1 000 кубических парсек пространства|
||1|total:S:5E4|total:S:1E6|Создай 1 миллион кубических парсек пространства|
||1|total:S:5E7|total:S:1E9|Создай 1 миллиард кубических парсек пространства|
||3|total:S:5E10|total:S:1E12|Создай 1 триллион кубических парсек пространства|
||3|total:S:5E13|total:S:1E15|Создай 1 квадриллион кубических парсек пространства|
||3|total:S:5E16|total:S:1E18|Создай 1 квинтиллион кубических парсек пространства|
||6|total:S:5E19|total:S:1E21|Создай 1 секстиллион кубических парсек пространства|
||6|total:S:5E22|total:S:1E24|Создай 1 септиллион кубических парсек пространства|
||1|total:He:1000|total:He:1000|Получи 1 миллиард гелия|
||3|total:He:1E12|total:He:1E12|Получи 1 квадриллион гелия|
||6|total:He:1E21|total:He:1E21|Получи 1 секстиллион гелия|
||1|||Израсходуй 1 миллион гелия|
||3|||Израсходуй 1 триллион гелия|
||6|||Израсходуй 1 квинтиллион гелия|
||1|total:Voids:5|total:Voids:25|Сформируй 25 войдов|
||1|total:Voids:25|total:Voids:125|Сформируй 125 войдов|
||1|total:Voids:125|total:Voids:625|Сформируй 625 войдов|
||3|total:Voids:625|total:Voids:3125|Сформируй 3125 войдов|
||3|total:Voids:3125|total:Voids:15625|Сформируй 15625 войдов|
||6|total:Voids:15625|total:Voids:78125|Сформируй 78125 войдов|Продолжай в том же духе, без войдов вселенная обречена!
|a33|10|energy::1E27|energy::1E27|На складе пусто!|Потрачен весь запас энергии.
|a5|1||total:Star:1|Зажги звезду|
|a6|1|total:OD:1|total:OD:1|Зажги оранжевого карлика|
|a7|1||total:YD:1|Зажги жёлтого карлика|
|a8|1|total:WYD:1|total:WYD:1|Зажги бело-жёлтого карлика|
|a9|1|total:YG:1|total:YG:1|Зажги жёлтого гиганта|
|a10|1||total:BG:1|Зажги голубого гиганта|
|a11|1|total:BG:1|total:SG:1|Зажги сверхгиганта|
|a12|1|total:SG:1|total:HG:1|Зажги гипергиганта|
|a13|1||total:WD:1|Получи белого карлика|
|a14|1||total:NS:1|Получи нейтронную звезду|
|a15|1|total:NS:1|total:MGT:1|Получи магнетар|
|a16|1||total:BH:1|Получи чёрную дыру|
|a17|1|total:BH:1|total:SMBH:1|Создай сверхмассивную чёрную дыру|
|a18|1||total:N:1|Создай туманность|
|a19|1|total:N:1|total:G:1|Создай глобулу|
|a20|1|total:G:1|total:STN:1|Создай звёздную колыбель|
|a21|1|total:G:1|total:MC:1|Создай молекулярное облако|
|a22|3|total:STN:1|total:ASTN:1|Создай активную звёздную колыбель|
|a23|3|total:MC:100|total:MC:1E4|Создай 10 000 молекулярных облаков|
|a34|1|auto:Star:1|auto:Star:10|Создай 10 звёзд с помощью облаков|
|a35|1|auto:Star:10|auto:Star:1000|Создай 1 000 звёзд с помощью облаков|
|a36|3|auto:Star:1000|auto:Star:1E6|Создай 1 миллион звёзд с помощью облаков|
|a37|1|total:Star:30|total:Star:300|Зажги 300 звёзд|
|a38|1|total:OD:1|total:OD:2000|Зажги 2 000 оранжевых карликов|
|a39|1|total:WYD:1|total:WYD:500|Зажги 500 бело-жёлтых карликов|
|a40|1|total:BD:1|total:BD:1E6|Зажги 1 миллион бурых карликов|
|a41|3|total:HG:1000|total:HG:1000|Это кому-нибудь нужно?|Зажги 1 000 гипергигантов.
|a42|3|total:Star:5000|total:Star:1E7|Зажги 10 миллионов звёзд|
||3|total:Star:4E9|total:Star:4E11|Зажги 400 миллиардов звёзд|
||3|total:Star:9E12|total:Star:9E14|Зажги 90 квадриллионов звёзд|
|a43|3|total:SMBH:1|total:SMBH:1E4|Сбрось 10 000 вещества в сверхмассивные чёрные дыры|
||3|total:SMBH:3E5|total:SMBH:4.2E7|Сбрось 42 миллиона вещества в сверхмассивные чёрные дыры|Примерно столько весит Стрелец А со звёздочкой.
||3|total:SMBH:3E9|total:SMBH:6.6E11|Сбрось 660 миллиардов вещества в сверхмассивные чёрные дыры|Примерно столько весит TON 618.
|a44|1||total:SC:1|Создай любое звёздное скопление|
|a45|1||total:SCM:1|Создай среднее звёздное скопление|
|a46|1|total:SC:1|total:SCL:1|Создай большое звёздное скопление|
|a47|3|total:SCL:1|total:SCG:1|Создай гигантское звёздное скопление|
|a48|3|total:SC:10|total:SC:100|Создай 100 звёздных скоплений|
|a49|1||total:GAL:1|Создай любую галактику|
|a50|3|total:GAL:1|total:GALS:1|Создай спиральную галактику|
|a51|1||total:GEM:1|Создай среднюю галактику|
|a52|1|total:GAL:1|total:GEL:1|Создай большую галактику|
|a53|3|total:GEL:1|total:GEG:1|Создай гигантскую галактику|
|a54|3|total:GAL:5|total:GAL:30|Создай 30 галактик|
|a55|1|total:GAL:30|total:LGG:1|Создай группу галактик|
|a56|3|total:LGG:1|total:LGG:30|Создай 30 групп галактик|
|a57|3|total:LGG:30|total:LSC:1|Создай галактическое сверхскопление|
|a58|3|total:LSC:1|total:LSC:30|Создай 30 галактических сверхскоплений|
|a59|6|total:LSC:30|total:LGF:1|Создай галактическую нить|
|a60|6|total:LGF:1|total:LGF:100|Создай 100 галактических нитей|
|a61|6|total:LGF:100|total:LGW:1|Создай великую стену|
|a62|6|total:LGW:1|total:LGW:100|Создай 100 великих стен|
|a63|6|total:LGW:100|total:LUB:1|Создай вселенский пузырь|
|a64|10|total:LUB:1|total:LUB:1|Это должно было когда-то закончиться!|Создана крупномасштабная структура вселенной.
1|a65|1||Planet:1|Создай любую планету|
1|a66|1|GG:1^AB:1|RP:1|Создай каменистую планету|
1|a67|1|RP:1|HP:1|Создай обитаемую планету|
1|a68|3||EON1:1|Создай жизнь|
1|a69|1|EON1:1|EON2:1|Вырасти амёб и водоросли|
1|a70|1|EON2:1|EON3:1|Вырасти трилобитов и рыб|
1|a71|1|EON3:1|EON4:1|Вырасти динозавров и птиц|
1|a72|1|EON4:1|EON5:1|Вырасти млекопитающих и приготовься...|
1|a73|1|Planet:1|Planet:1E5|Создай 100 000 планет|
1|a74|1|HP:1|HP:1E4|Создай 10 000 обитаемых планет|
1|a75|3|EON1:1|EON1:1000|Создай жизнь на 1 000 планет|
1|a76|6|C0.0:1|C0.0:1|Возник разум!|
1|a77|1|C0.0:1|C0.5:1|Они умеют лепить глиняные горшки!|Цивилизация достигла уровня 0.5.
1|a78|3|C0.7:1|C1.0:1|Они освоили свою родную планету!|Цивилизация достигла уровня 1.0.
1|a79|1|C1.3:1|C1.5:1|Они построили космический город!|Цивилизация достигла уровня 1.5.
1|a80|6|C1.7:1|C2.0:1|У них есть рой Дайсона!|Цивилизация достигла уровня 2.0.
1|a81|1|C2.2:1|C2.2:1|Вроде бы они начали расти!|Цивилизация заселила сотню звёзд.
1|a82|1|C2.5:1|C2.5:1|Их почти можно увидеть невооружённым глазом!|Цивилизация заселила среднее звёздное скопление.
1|a83|3|C2.8:1|C2.8:1|О, они наконец-то поняли, как управлять гравитацией!|Цивилизация достигла уровня 2.8.
1|a84|6|C3.0:1|C3.0:1|Они и правда научились летать!|Цивилизация способна путешествовать между галактиками.
1|a85|3|C3.2:1|C3.2:1|Они наверное так гордятся собой!|Цивилизация расселилась на масштабах порядка Туманности Андромеды.
1|a86|3|C3.5:1|C3.5:1|Вот это они смекалистые!|Цивилизация использует ресурсы нейтронной звезды.
1|a87|3|C3.8:1|C3.8:1|Теперь их и правда легко заметить.|Цивилизация заселила сотни тысяч галактик.
1|a88|6|C4.0:1|C4.0:1|Кажется это уже было, нет?|Цивилизация освоила манипуляции со временем.
1|a89|6|C4.2:1|C4.2:1|Наконец-то они перестали мелочиться.|Цивилизация использует сверхмассивную чёрную дыру для своих целей.
1|a90|10|C4.4:1|C4.4:1|Они справились! Интересно, чем же дальше займутся?|Цивилизация расселилась на масштабах порядка всей видимой вселенной.
1|a91|3|C1.6:1|SET:1|Они основали колонию у соседней звезды!|
1|a92|6||GSM:1&C2.8:50|Они похоже основали Галактическую Федерацию или что-то в этом роде!|
1|a93|1|||О нет...|Погибла цивилизация 0.8+ уровня.
1|a94|1|||Да сколько можно!|Погибло 100 цивилизаций 1.2+ уровня.
1|a95|1|||К этому вообще можно привыкнуть?|Погибло 1 000 цивилизаций 1.6+ уровня.
1|a96|3|||Да, в какой-то момент становится безразлично.|Погибло 10 000 цивилизаций 2.0+ уровня
1|a97|6|||А выглядели крепкими. Что ж поделать.|Погибла цивилизация 3.0+ уровня.
1|a98|10|||Моё разочарование неизмеримо.|Погибла цивилизация 4.0+ уровня.
1|a99|6|||Что-то это мне напоминает...|На момент 13.8 миллиардов лет существует цивилизация уровня 0.9.
1|a102|3|высокая плотность|BigCrunch|Большое сжатие!|Вселенная схлопнулась в колоссальную чёрную дыру.
1|a103|3|низкая плотность|BigRip|Большой разрыв!|Вселенная рассеялась в разные стороны.
1|a104|6|BigCrunch^BigRip|BigCrunch&BigRip|Двойная неурядица!|Два разных апокалипсиса завершены.
1||10|||Изгнание! Вас выгнали из вселенной.|Теперь эта вселенная существует сама по себе, без вашего участия.
1||3|||Антитеизм: они отрицают ваше существование.|Может они в чём-то и правы?
1||3|||Интеграция: они стремятся к сосуществованию.|Может быть хотя бы у них получится?
1||3|||Ксеноцид: их воинственность приводит к ужасным результатам.|Впрочем, ничего нового.
1||3|||Реконструкция: они переделывают вселенную.|Это же их вселенная, да? Или нет?
1||3|||Духовность: они ищут глубинный смысл.|Что если они узнают правду?
1||3|||Исход: они хотят покинуть вселенную.|Что же им здесь не нравится?
1||6|||Динамичная вселенная|Все тенденции проявили себя!
1||3|||Изгнание: они пытаются выгнать из вселенной вас!|Их определённо что-то не устраивает.
1||3|||Федерация: они ищут способы мирного сосуществования.|Наверное это неплохо?
1||3|||Просвещение: они помогают отстающим.|Какая прелесть!
1||3|||Новый порядок: они закапывают топор войны.|Может оно и к лучшему?
1||3|||Великие войны: они устроили эпическое побоище.|Жаль, что отсюда не рассмотреть всех деталей.
1||3|||Истребление: они занимаются чисткой потенциальных конкурентов.|Жизнь — это возобновляемый ресурс.
1||3|||Опустошение: они разрушают вселенную!|Звёзды превращаются в чёрные дыры.
1||3|||Звёздное дробление: они перестраивают вселенную под себя.|Им удобнее жить на малых и стабильных звёздах.
1||3|||Звёздная накачка: они создают новые звёзды!|Из облаков и бурых карликов создаются красные карлики.
1||3|||Звёздное охлаждение: они стабилизируют звёзды.|Снижается активность звёздных процессов.
1||3|||Молитва: они невероятно истово молятся.|Вы даже смогли что-то расслышать.
1||3|||Диалог: они ждут ответа!|А отвечать именно вам.
1||3|||Живая вселенная|Произошло 10 действий цивилизаций.
1||6|||Очень живая вселенная|Произошло 30 действий цивилизаций.
1||10|||Невероятно живая вселенная!|Произошло 100 действий цивилизаций.
1||10|||Столько всего происходит!|Все действия произошли хотя бы один раз!
||1|сила получена|сила применена|Научись быстро копать.|Используй лопатин
||1|сила получена|сила применена|Запусти инфляцию.|Используй надувин
||3|сила получена|сила применена|Пропусти время.|Используй временит
||3|сила получена|сила применена|Утилизируй мусор.|Используй столкновит
||6|сила получена|сила применена|Подели физику на ноль.|Используй первекс
||1|сила получена|сила применена|Уничтожь пространство.|Используй сингулярит
||1|сила получена|сила применена|Укрепи структуры.|Используй обновин
||3|сила получена|сила применена|Разожги звёзды.|Используй вливанит
||3|сила получена|сила применена|Остуди вселенную.|Используй морозин
||6|сила получена|сила применена|Избавься от звёзд.|Используй дезвездит
||1|сила получена|сила применена|Сделай больше планет.|Используй камнекс
||1|сила получена|сила применена|Осемени вселенную.|Используй панспермин
||3|сила получена|сила применена|Удобри вселенную.|Используй гайанит
||3|сила получена|сила применена|Взбодри вселенную.|Используй дарвинит
||6|сила получена|сила применена|Наполни вселенную благостью.|Используй эдемекс
||1|сила получена|сила применена|Научи разумных жить в мире.|Используй пацифин
||1|сила получена|сила применена|Научи разумных сражаться за свою жизнь.|Используй озверин
||3|сила получена|сила применена|Назначь пророков среди разумных.|Используй моралит
||3|сила получена|сила применена|Покарай тех разумных, кто не следует твоим планам.|Используй дискриминит
||6|сила получена|сила применена|Подави тех разумных, которые слишком много о себе возомнили.|Используй репрекс
||6|одна любая сила получена в 4 градациях|флэш|Потрать силу одного типа по нарастанию градаций.|Зелёная, синяя, фиолетовая, оранжевая
||3|снижена вручную|Bur=1|Холодная вселенная.|Минимальная горючесть
||3|аналогично|Bur=19|Звёзды зажигают, чтобы они сгорели!|Максимальная горючесть
||3|аналогично|Exp=1|Плотный вакуум.|Минимальная взрывчатость
||3|аналогично|Exp=19|Бада-бим, бада-бум-бум-бум!|Максимальная взрывчатость
||3|аналогично|Agg=1|Мир и покой.|Минимальная агрессия
||3|аналогично|Agg=19|Как 4Е4!|Максимальная агрессия
1|a101|6|||Все силы доступны одновременно!|
1|a100|10|||Все силы использованы хотя бы по одному разу в этой вселенной.|
||1|3 обычные силы одного вида||Улучшите силу до редкой.|
||3|3 редкие силы одного вида||Улучшите силу до эпической.|
||6|3 эпические силы одного вида||Улучшите силу до легендарной.|
||1|улучш 3 силы|5|Улучшите 5 сил.|
||1|8|13|Улучшите 13 сил.|
||3|21|34|Улучшите 34 силы.|
||3|55|89|Улучшите 89 сил.|
||6|144|233|Улучшите 233 силы.|
||6|377|610|Улучшите 610 сил.|
||10|987|1597|Улучшите 1 597 сил.|Продолжайте в том же духе!
||1|1|3|Улучшите 3 сундука с силами.|
||3|5|7|Улучшите 7 сундуков с силами.|
||6|14|20|Улучшите 20 сундуков с силами.|
||10|38|55|Улучшите 55 сундуков с силами.|Спасибо за поддержку!
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More