Add life rules and implement tests for civilization and life states
- Introduced a new file `life_rules.txt` to define life progression and degradation rules. - Created `test_civilization_state.gd` to validate the functionality of civilization rules and ethics. - Added `test_life.gd` to test life evolution, including initialization, batch processing, and state validation. - Implemented `civilization_state.gd` to manage civilization ethics and space civilization detachment logic. - Ensured all tests check for expected outcomes and validate state integrity after operations.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
extends RefCounted
|
||||
|
||||
const ETHICS = ["m", "l", "w", "d", "c", "b"]
|
||||
|
||||
static func is_space_civilization(id: String, rules: Dictionary) -> bool:
|
||||
return rules.has(id) and str(rules[id].home) != ""
|
||||
|
||||
# Preserve any existing ethics. New populations start equally distributed;
|
||||
# reductions (including blasts) are proportional and never leave negative counts.
|
||||
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void:
|
||||
if not target.has("ethics"):
|
||||
target.ethics = {}
|
||||
var physical: Dictionary = world._physical_amounts(target)
|
||||
for id in rules:
|
||||
var count: float = physical.get(id, 0.0)
|
||||
var current: Dictionary = target.ethics.get(id, {})
|
||||
var total: float = 0.0
|
||||
for ethic in ETHICS:
|
||||
current[ethic] = maxf(0.0, current.get(ethic, 0.0))
|
||||
total += current[ethic]
|
||||
if total > count:
|
||||
current = world._allocate_whole(current, count)
|
||||
elif total < count:
|
||||
var missing: float = count - total
|
||||
var base: float = floorf(missing / ETHICS.size())
|
||||
var rest: int = int(missing - base * ETHICS.size())
|
||||
for i in ETHICS.size():
|
||||
current[ETHICS[i]] += base + (1.0 if i < rest else 0.0)
|
||||
target.ethics[id] = current
|
||||
|
||||
static func detach_space_civilizations(target: Dictionary, rules: Dictionary) -> void:
|
||||
for held: Dictionary in target.binds.values():
|
||||
for id in held:
|
||||
if is_space_civilization(id, rules):
|
||||
target.amount[id] = target.amount.get(id, 0.0) + held[id]
|
||||
held[id] = 0.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://bw88euxvptt51
|
||||
@@ -1,5 +1,6 @@
|
||||
extends Node
|
||||
class_name Owniverse
|
||||
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
|
||||
#var state: State = State.new()
|
||||
@@ -62,8 +63,26 @@ func migrate_time_precision() -> void:
|
||||
_evolute_to_datetime(float(state.time.datetime))
|
||||
|
||||
|
||||
func _evolute_to_datetime(datetime: float) -> void:
|
||||
# Old saves have counts but no life ages. Start only missing cohorts at load time.
|
||||
func initialize_life_batches() -> void:
|
||||
CivilizationState.detach_space_civilizations(state, Global.CivilizationRules)
|
||||
CivilizationState.reconcile(self, state, Global.CivilizationRules)
|
||||
if state.get("life_version", 0) >= 2:
|
||||
return
|
||||
var physical: Dictionary = _physical_amounts(state)
|
||||
for id in entities:
|
||||
if entities[id].life_rule.is_empty():
|
||||
continue
|
||||
var tracked: float = 0.0
|
||||
for batch: Dictionary in state.batches.get(id, []):
|
||||
tracked += batch.amount
|
||||
_append_batch(state, id, maxf(0.0, physical.get(id, 0.0) - tracked), state.time.datetime, state.time.datetime)
|
||||
state.life_version = 2
|
||||
new_state = state
|
||||
|
||||
|
||||
func _evolute_to_datetime(datetime: float) -> void:
|
||||
initialize_life_batches()
|
||||
var previous: Dictionary = state
|
||||
var next: Dictionary = previous.duplicate_deep()
|
||||
next.version = SimulationTime.STATE_VERSION
|
||||
@@ -379,7 +398,61 @@ func _add_output_terms(outputs: Dictionary, terms: String, count: float) -> void
|
||||
outputs[id] = outputs.get(id, 0.0) + count * amount
|
||||
|
||||
|
||||
# One life attempt: progress, degradation, or retry at the same stage.
|
||||
# Retried worlds are not new planets and must not increment production statistics.
|
||||
func _life_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||
var rule: Dictionary = entity.life_rule
|
||||
var pending: Dictionary = {}
|
||||
if not target.has("life_remainders"):
|
||||
target.life_remainders = {}
|
||||
for channel in ["progress", "degradation"]:
|
||||
var key: String = entity.id + ":" + channel
|
||||
var expected: float = count * float(rule[channel]) + target.life_remainders.get(key, 0.0)
|
||||
target.life_remainders[key] = expected
|
||||
pending[channel] = floorf(snappedf(expected, 1.0e-12))
|
||||
var assigned: Dictionary = _allocate_whole(pending, count)
|
||||
for channel in assigned:
|
||||
var key: String = entity.id + ":" + channel
|
||||
target.life_remainders[key] = maxf(0.0, target.life_remainders[key] - assigned[channel])
|
||||
var result: Dictionary = {entity.id: count - assigned.progress - assigned.degradation}
|
||||
result[rule.next] = assigned.progress
|
||||
var weights: Dictionary = {}
|
||||
var weight_sum: float = 0.0
|
||||
for term in str(rule.fall).split("^"):
|
||||
var pair: PackedStringArray = term.split(":")
|
||||
weights[pair[0]] = float(pair[1])
|
||||
weight_sum += float(pair[1])
|
||||
var requests: Dictionary = {}
|
||||
for id in weights:
|
||||
var key: String = entity.id + ":fall:" + id
|
||||
var expected: float = assigned.degradation * weights[id] / weight_sum + target.life_remainders.get(key, 0.0)
|
||||
target.life_remainders[key] = expected
|
||||
requests[id] = maxf(0.0, floorf(snappedf(expected, 1.0e-12)))
|
||||
var falls: Dictionary = _allocate_whole(requests, assigned.degradation)
|
||||
var distributed: float = 0.0
|
||||
for id in falls:
|
||||
distributed += falls[id]
|
||||
result[id] = result.get(id, 0.0) + falls[id]
|
||||
var key: String = entity.id + ":fall:" + id
|
||||
target.life_remainders[key] -= falls[id]
|
||||
# Fill sub-unit fall quotas deterministically, retaining signed allocation errors.
|
||||
while distributed < assigned.degradation:
|
||||
var selected: String = ""
|
||||
var largest: float = -INF
|
||||
for id in weights:
|
||||
var rest: float = target.life_remainders[entity.id + ":fall:" + id]
|
||||
if rest > largest:
|
||||
largest = rest
|
||||
selected = id
|
||||
result[selected] = result.get(selected, 0.0) + 1.0
|
||||
target.life_remainders[entity.id + ":fall:" + selected] -= 1.0
|
||||
distributed += 1.0
|
||||
return result
|
||||
|
||||
|
||||
func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary:
|
||||
if not entity.life_rule.is_empty():
|
||||
return _life_products(target, entity, count)
|
||||
var outputs: Dictionary = {}
|
||||
for id in entity.output_and:
|
||||
outputs[id] = count * maxf(0.0, entity.output_and[id])
|
||||
@@ -587,12 +660,12 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
incoming[heir] = {}
|
||||
incoming[heir][id] = incoming[heir].get(id, 0.0) + released * (1.0 - fraction) * heirs[heir]
|
||||
for child in outputs.get(id, {}):
|
||||
if entities.has(child):
|
||||
if entities.has(child) and not CivilizationState.is_space_civilization(child, Global.CivilizationRules):
|
||||
incoming[heir][child] = incoming[heir].get(child, 0.0) + released * fraction * heirs[heir] * outputs[id][child]
|
||||
var remaining: float = held * owner_survival * (1.0 - fraction)
|
||||
target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + remaining
|
||||
for successor in outputs.get(id, {}):
|
||||
if not entities.has(successor):
|
||||
if not entities.has(successor) or CivilizationState.is_space_civilization(successor, Global.CivilizationRules):
|
||||
continue
|
||||
var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor])
|
||||
if not incoming.has(owner_id):
|
||||
@@ -614,7 +687,10 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
continue
|
||||
if not entities.has(successor):
|
||||
continue
|
||||
amount = _credit(target, successor, amount)
|
||||
if successor == id and not entities[id].life_rule.is_empty():
|
||||
target.amount[id] = target.amount.get(id, 0.0) + amount
|
||||
else:
|
||||
amount = _credit(target, successor, amount)
|
||||
var before_batches: Array = target.batches.get(successor, []).duplicate(true)
|
||||
_append_batch(target, successor, amount, part.from, part.to)
|
||||
_produce_newborn_lifetime(target, successor, amount, part.from, part.to, to_year, before_batches)
|
||||
@@ -629,6 +705,7 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
|
||||
_bind_new_owners(target, new_owners)
|
||||
_round_object_allocations(target)
|
||||
_apply_blast(target, blast_power, environment)
|
||||
CivilizationState.reconcile(self, target, Global.CivilizationRules)
|
||||
return true
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ var cost: Dictionary = {}
|
||||
var bind: Dictionary = {}
|
||||
var mass: float
|
||||
var space: int
|
||||
var life_rule: Dictionary = {}
|
||||
var lifespan: float
|
||||
var lifespan_delta: float
|
||||
var auto: float
|
||||
@@ -42,7 +43,8 @@ func _init(values: Dictionary) -> void:
|
||||
#print(id, " | ", cost, " | ", bind)
|
||||
mass = float(values.mass)
|
||||
space = int(values.space)
|
||||
lifespan = float(values.lifespan)
|
||||
life_rule = Global.LifeRules.get(id, {}).duplicate(true)
|
||||
lifespan = float(life_rule.span) if not life_rule.is_empty() else float(values.lifespan)
|
||||
lifespan_delta = lifespan / Global.batch_count if lifespan > 10 else lifespan
|
||||
auto = float(values.auto)
|
||||
gamma = float(values.gamma)
|
||||
|
||||
Reference in New Issue
Block a user