Files
aetherbound-guild/runtime/p10_2/run_state.gd
T

533 lines
19 KiB
GDScript

class_name P102RunState
extends RefCounted
const P101State = preload("res://p10_1/region_run_state.gd")
const STATE_KIND := "p102_siltwake_mid_contract"
const RULESET_VERSION := "p10.2-siltwake-mid-contract-r1"
var state_kind := STATE_KIND
var ruleset := RULESET_VERSION
var revision := 0
var lineage_id := ""
var generation_root_seed := ""
var next_region_run_ordinal := 0
var migration_source_hash := ""
var migration_source_revision := -1
var guild_profile: Dictionary = {}
var current_run: Dictionary = {}
var operation_receipts: Dictionary = {}
var legacy_operation_receipts: Dictionary = {}
var legacy_entry_commits: Dictionary = {}
var battle_checkpoints: Dictionary = {}
func initialize_from_p101(p101_snapshot: Dictionary, market_board: Dictionary) -> bool:
var probe := P101State.new()
if not probe.restore_snapshot(p101_snapshot):
return false
var source := probe.snapshot()
var previous_run: Dictionary = source.current_run
if (
previous_run.is_empty()
or String(previous_run.phase) != "market_2"
or int(previous_run.market_index) != 2
or previous_run.get("market_2_handoff", {}).is_empty()
or String(market_board.get("run_seed", "")) != String(previous_run.run_seed)
or int(market_board.get("market_index", -1)) != 2
or int(market_board.get("refresh_index", -1)) != 0
):
return false
state_kind = STATE_KIND
ruleset = RULESET_VERSION
revision = 0
lineage_id = String(source.lineage_id)
generation_root_seed = String(source.generation_root_seed)
next_region_run_ordinal = int(source.next_region_run_ordinal)
migration_source_hash = _hash(source)
migration_source_revision = int(source.revision)
guild_profile = Dictionary(source.guild_profile).duplicate(true)
operation_receipts = {}
legacy_operation_receipts = Dictionary(source.operation_receipts).duplicate(true)
legacy_entry_commits = Dictionary(source.entry_commits).duplicate(true)
battle_checkpoints = {}
var company: Array = Array(previous_run.company).duplicate()
var deployed: Array = Array(previous_run.deployed_recruit_ids).duplicate()
var reserve: Array = Array(previous_run.reserve_recruit_ids).duplicate()
var handoff: Dictionary = Dictionary(previous_run.market_2_handoff).duplicate(true)
var last_report: Dictionary = Dictionary(previous_run.last_battle_report).duplicate(true)
current_run = {
"run_id": String(previous_run.run_id),
"region_id": "RG-02",
"contract_id": "CTR-R2-01",
"difficulty": String(previous_run.difficulty),
"run_seed": String(previous_run.run_seed),
"status": "active",
"phase": "market",
"market_index": 2,
"round_index": 2,
"cleared_rounds": 1,
"coin": int(previous_run.coin),
"replacement_credit": 0,
"refresh_count": 0,
"line_capacity": 4,
"market_board": market_board.duplicate(true),
"available_recruit_offer_ids": _offer_ids(market_board.recruit_offers, "source_offer_id"),
"available_equipment_offer_ids": _offer_ids(market_board.equipment_offers, "offer_id"),
"available_battle_offer_ids": _offer_ids(market_board.battle_offers, "offer_id"),
"locked_offer_ids": [],
"company": company,
"deployed_recruit_ids": deployed,
"reserve_recruit_ids": reserve,
"recruit_records": Dictionary(previous_run.recruit_records).duplicate(true),
"inventory": Dictionary(previous_run.inventory).duplicate(true),
"equipped_by_recruit": Dictionary(previous_run.equipped_by_recruit).duplicate(true),
"formation_proposal": deployed.duplicate(),
"selected_route_id": "",
"pending_encounter": {},
"pre_battle_checkpoint_id": "",
"recovery_required": false,
"recovery_cost": 0,
"last_battle_report": last_report,
"first_crossing_handoff": handoff,
"route_history": [{
"round_index": 1,
"route_id": String(last_report.get("route_id", "")),
"plan_id": String(last_report.get("plan_id", "")),
"outcome": String(last_report.get("outcome", "")),
"battle_hash": String(handoff.get("source_battle_hash", "")),
"reward_coin": int(handoff.get("reward_coin", 0)),
}],
"recruits_hired_total": company.size(),
"recruits_fallen_total": 0,
"recruits_dismissed_total": 0,
"reed_ferry_restored": false,
}
return invariants_hold()
func snapshot() -> Dictionary:
var raw := {
"state_kind": state_kind,
"ruleset": ruleset,
"revision": revision,
"lineage_id": lineage_id,
"generation_root_seed": generation_root_seed,
"next_region_run_ordinal": next_region_run_ordinal,
"migration_source_hash": migration_source_hash,
"migration_source_revision": migration_source_revision,
"guild_profile": guild_profile.duplicate(true),
"current_run": current_run.duplicate(true),
"operation_receipts": operation_receipts.duplicate(true),
"legacy_operation_receipts": legacy_operation_receipts.duplicate(true),
"legacy_entry_commits": legacy_entry_commits.duplicate(true),
"battle_checkpoints": battle_checkpoints.duplicate(true),
}
var normalized = JSON.parse_string(JSON.stringify(raw))
return Dictionary(normalized).duplicate(true) if normalized is Dictionary else {}
func restore_snapshot(raw: Dictionary) -> bool:
var normalized_value = JSON.parse_string(JSON.stringify(raw))
if not normalized_value is Dictionary:
return false
var normalized: Dictionary = normalized_value
var required := [
"state_kind", "ruleset", "revision", "lineage_id",
"generation_root_seed", "next_region_run_ordinal",
"migration_source_hash", "migration_source_revision", "guild_profile",
"current_run", "operation_receipts", "legacy_operation_receipts",
"legacy_entry_commits", "battle_checkpoints",
]
for key in required:
if not normalized.has(key):
return false
for key in [
"guild_profile", "current_run", "operation_receipts",
"legacy_operation_receipts", "legacy_entry_commits", "battle_checkpoints",
]:
if not normalized[key] is Dictionary:
return false
var before := snapshot()
_assign(normalized)
if not invariants_hold():
if not before.is_empty():
_assign(before)
return false
return true
func invariants_hold() -> bool:
if state_kind != STATE_KIND or ruleset != RULESET_VERSION or revision < 0:
return false
if not _is_hex(lineage_id, 32) or not _is_hex(generation_root_seed, 32):
return false
if next_region_run_ordinal < 0 or not _is_hex(migration_source_hash, 64) or migration_source_revision < 0:
return false
if not _guild_invariants_hold() or not _receipts_hold() or not _checkpoints_hold():
return false
return _run_invariants_hold()
func _guild_invariants_hold() -> bool:
for key in [
"persistent_renown", "contract_clear_ids", "discovery_ids",
"company_records", "unlocked_region_ids", "clear_receipts",
]:
if not guild_profile.has(key):
return false
if not guild_profile.clear_receipts is Dictionary:
return false
return (
"RG-02" in guild_profile.unlocked_region_ids
and "CLEAR-RG01-C01" in guild_profile.contract_clear_ids
and "CLEAR-RG01-C02" in guild_profile.contract_clear_ids
and _unique(guild_profile.unlocked_region_ids)
and _unique(guild_profile.contract_clear_ids)
)
func _run_invariants_hold() -> bool:
var required := [
"run_id", "region_id", "contract_id", "difficulty", "run_seed",
"status", "phase", "market_index", "round_index", "cleared_rounds",
"coin", "replacement_credit", "refresh_count", "line_capacity",
"market_board", "available_recruit_offer_ids",
"available_equipment_offer_ids", "available_battle_offer_ids",
"locked_offer_ids", "company", "deployed_recruit_ids",
"reserve_recruit_ids", "recruit_records", "inventory",
"equipped_by_recruit", "formation_proposal", "selected_route_id",
"pending_encounter", "pre_battle_checkpoint_id", "recovery_required",
"recovery_cost", "last_battle_report", "first_crossing_handoff",
"route_history", "recruits_hired_total", "recruits_fallen_total",
"recruits_dismissed_total", "reed_ferry_restored",
]
for key in required:
if not current_run.has(key):
return false
for key in [
"market_board", "recruit_records", "inventory", "equipped_by_recruit",
"pending_encounter", "last_battle_report", "first_crossing_handoff",
]:
if not current_run[key] is Dictionary:
return false
for key in [
"available_recruit_offer_ids", "available_equipment_offer_ids",
"available_battle_offer_ids", "locked_offer_ids", "company",
"deployed_recruit_ids", "reserve_recruit_ids", "formation_proposal",
"route_history",
]:
if not current_run[key] is Array:
return false
if not _is_hex(String(current_run.run_id), 32) or not _is_hex(String(current_run.run_seed), 32):
return false
if String(current_run.region_id) != "RG-02" or String(current_run.contract_id) != "CTR-R2-01":
return false
if String(current_run.status) != "active" or String(current_run.phase) not in ["market", "prepared", "recovery", "market_5"]:
return false
var market_index := int(current_run.market_index)
var cleared_rounds := int(current_run.cleared_rounds)
if market_index < 2 or market_index > 5 or int(current_run.round_index) != market_index:
return false
if cleared_rounds != market_index - 1:
return false
if int(current_run.coin) < 0 or int(current_run.replacement_credit) < 0 or int(current_run.refresh_count) < 0:
return false
var expected_capacity := mini(10, 4 + floori(float(cleared_rounds) / 2.0))
if int(current_run.line_capacity) != expected_capacity:
return false
if bool(current_run.reed_ferry_restored) != (market_index == 5):
return false
if int(current_run.recruits_hired_total) < current_run.company.size():
return false
if int(current_run.recruits_fallen_total) < 0 or int(current_run.recruits_dismissed_total) < 0:
return false
if current_run.first_crossing_handoff.is_empty() or current_run.route_history.size() != cleared_rounds:
return false
if String(current_run.route_history[0].get("battle_hash", "")) != String(current_run.first_crossing_handoff.get("source_battle_hash", "")):
return false
if not _board_invariants_hold():
return false
if not _company_invariants_hold():
return false
var phase := String(current_run.phase)
if phase == "market":
return (
market_index >= 2
and market_index <= 4
and String(current_run.selected_route_id).is_empty()
and current_run.pending_encounter.is_empty()
and String(current_run.pre_battle_checkpoint_id).is_empty()
and not bool(current_run.recovery_required)
and int(current_run.recovery_cost) == 0
)
var checkpoint_id := String(current_run.pre_battle_checkpoint_id)
if phase == "prepared":
return (
market_index >= 2
and market_index <= 4
and String(current_run.selected_route_id) in current_run.available_battle_offer_ids
and not current_run.pending_encounter.is_empty()
and String(current_run.pending_encounter.get("offer_id", "")) == String(current_run.selected_route_id)
and checkpoint_id in battle_checkpoints
and not current_run.deployed_recruit_ids.is_empty()
)
if phase == "recovery":
return (
market_index >= 2
and market_index <= 4
and bool(current_run.recovery_required)
and int(current_run.recovery_cost) > 0
and String(current_run.selected_route_id).is_empty()
and current_run.pending_encounter.is_empty()
and checkpoint_id in battle_checkpoints
and not current_run.last_battle_report.is_empty()
)
return (
market_index == 5
and String(current_run.selected_route_id).is_empty()
and current_run.pending_encounter.is_empty()
and String(current_run.pre_battle_checkpoint_id).is_empty()
and not bool(current_run.recovery_required)
and int(current_run.recovery_cost) == 0
)
func _board_invariants_hold() -> bool:
var board: Dictionary = current_run.market_board
if int(current_run.market_index) == 5:
for key in [
"handoff_kind", "board_hash", "run_seed", "contract_id", "region_id",
"market_index", "cleared_rounds", "line_capacity",
"source_settlement_hash", "carried_lock_ids",
]:
if not board.has(key):
return false
if (
String(board.handoff_kind) != "p10.2.market_5_continuation"
or String(board.run_seed) != String(current_run.run_seed)
or int(board.market_index) != 5
or int(board.cleared_rounds) != 4
or int(board.line_capacity) != 6
):
return false
var market_5_hash := String(board.board_hash)
var market_5_candidate := board.duplicate(true)
market_5_candidate.erase("board_hash")
return (
market_5_hash == _hash(market_5_candidate)
and current_run.available_recruit_offer_ids.is_empty()
and current_run.available_equipment_offer_ids.is_empty()
and current_run.available_battle_offer_ids.is_empty()
and current_run.locked_offer_ids.is_empty()
)
for key in [
"board_id", "board_hash", "run_seed", "contract_id", "region_id",
"market_index", "round_index", "cleared_rounds_before", "line_capacity",
"offer_level", "refresh_index", "recruit_offers", "equipment_offers",
"battle_offers", "tide_disclosure", "generation_authority",
]:
if not board.has(key):
return false
if (
String(board.run_seed) != String(current_run.run_seed)
or int(board.market_index) != int(current_run.market_index)
or int(board.round_index) != int(current_run.round_index)
or int(board.refresh_index) != int(current_run.refresh_count)
or int(board.line_capacity) != int(current_run.line_capacity)
):
return false
if not board.recruit_offers is Array or board.recruit_offers.size() != 3:
return false
if not board.equipment_offers is Array or board.equipment_offers.size() != 4:
return false
if not board.battle_offers is Array or board.battle_offers.size() != 3:
return false
var recruit_ids := _offer_ids(board.recruit_offers, "source_offer_id")
var equipment_ids := _offer_ids(board.equipment_offers, "offer_id")
var battle_ids := _offer_ids(board.battle_offers, "offer_id")
if (
not _subset(current_run.available_recruit_offer_ids, recruit_ids)
or not _subset(current_run.available_equipment_offer_ids, equipment_ids)
or not _subset(current_run.available_battle_offer_ids, battle_ids)
):
return false
var lock_authority: Array = (
Array(current_run.available_recruit_offer_ids).duplicate()
+ Array(current_run.available_equipment_offer_ids).duplicate()
)
return (
_unique(current_run.locked_offer_ids)
and current_run.locked_offer_ids.size() <= 3
and _subset(current_run.locked_offer_ids, lock_authority)
)
func _company_invariants_hold() -> bool:
var company: Array = current_run.company
var deployed: Array = current_run.deployed_recruit_ids
var reserve: Array = current_run.reserve_recruit_ids
if not _unique(company) or not _unique(deployed) or not _unique(reserve):
return false
if company.is_empty() or deployed.is_empty() or deployed.size() > int(current_run.line_capacity):
return false
if not _subset(deployed, company) or not _subset(reserve, company):
return false
if not _same_members(deployed + reserve, company) or not _disjoint(deployed, reserve):
return false
if current_run.recruit_records.size() != company.size() or current_run.equipped_by_recruit.size() != company.size():
return false
for recruit_id in company:
if not current_run.recruit_records.has(recruit_id) or not current_run.equipped_by_recruit.has(recruit_id):
return false
var record: Dictionary = current_run.recruit_records[recruit_id]
if (
String(record.get("state", "")) != "living"
or int(record.get("level", 0)) < 1
or int(record.get("level", 0)) > 10
or int(record.get("xp", -1)) < 0
or int(record.get("current_hp", 0)) < 1
or int(record.get("current_hp", 0)) > int(record.get("max_hp", 0))
or int(record.get("survived_battles", -1)) < 0
):
return false
var worn := {}
for instance_id in current_run.inventory:
var item = current_run.inventory[instance_id]
if not item is Dictionary or String(item.get("instance_id", "")) != instance_id:
return false
var wearer := String(item.get("wearer_id", ""))
if not wearer.is_empty():
if wearer not in company or worn.has(wearer):
return false
if String(current_run.equipped_by_recruit.get(wearer, "")) != instance_id:
return false
worn[wearer] = instance_id
for recruit_id in current_run.equipped_by_recruit:
var equipped_id := String(current_run.equipped_by_recruit[recruit_id])
if not equipped_id.is_empty():
if not current_run.inventory.has(equipped_id):
return false
if String(current_run.inventory[equipped_id].wearer_id) != recruit_id:
return false
return true
func _receipts_hold() -> bool:
for operation_id in operation_receipts:
var row = operation_receipts[operation_id]
if not row is Dictionary or String(row.get("operation_id", "")) != operation_id:
return false
if (
String(row.get("kind", "")).is_empty()
or not _is_hex(String(row.get("intent_hash", "")), 64)
or String(row.get("status", "")) != "applied"
or String(row.get("code", "")).is_empty()
or int(row.get("revision", -1)) < 1
or int(row.get("revision", -1)) > revision
or not row.get("receipt", {}) is Dictionary
):
return false
return true
func _checkpoints_hold() -> bool:
for checkpoint_id in battle_checkpoints:
var row = battle_checkpoints[checkpoint_id]
if not row is Dictionary or String(row.get("checkpoint_id", "")) != checkpoint_id:
return false
if not _is_hex(checkpoint_id, 32) or not row.get("run", {}) is Dictionary:
return false
if not _is_hex(String(row.get("checkpoint_hash", "")), 64):
return false
var candidate: Dictionary = row.duplicate(true)
var stored_hash := String(candidate.checkpoint_hash)
candidate.erase("checkpoint_hash")
if _hash(candidate) != stored_hash:
return false
return true
func _assign(raw: Dictionary) -> void:
state_kind = String(raw.state_kind)
ruleset = String(raw.ruleset)
revision = int(raw.revision)
lineage_id = String(raw.lineage_id)
generation_root_seed = String(raw.generation_root_seed)
next_region_run_ordinal = int(raw.next_region_run_ordinal)
migration_source_hash = String(raw.migration_source_hash)
migration_source_revision = int(raw.migration_source_revision)
guild_profile = Dictionary(raw.guild_profile).duplicate(true)
current_run = Dictionary(raw.current_run).duplicate(true)
operation_receipts = Dictionary(raw.operation_receipts).duplicate(true)
legacy_operation_receipts = Dictionary(raw.legacy_operation_receipts).duplicate(true)
legacy_entry_commits = Dictionary(raw.legacy_entry_commits).duplicate(true)
battle_checkpoints = Dictionary(raw.battle_checkpoints).duplicate(true)
func _offer_ids(offers: Array, field: String) -> Array[String]:
var result: Array[String] = []
for offer in offers:
result.append(String(offer.get(field, "")))
return result
func _subset(values: Array, authority: Array) -> bool:
for value in values:
if value not in authority:
return false
return true
func _disjoint(left: Array, right: Array) -> bool:
for value in left:
if value in right:
return false
return true
func _same_members(left: Array, right: Array) -> bool:
return left.size() == right.size() and _subset(left, right) and _subset(right, left)
func _unique(values: Array) -> bool:
var seen := {}
for value in values:
if seen.has(value):
return false
seen[value] = true
return true
func _is_hex(value: String, length: int) -> bool:
if value.length() != length:
return false
for character in value:
if character not in "0123456789abcdef":
return false
return true
func _hash(value) -> String:
return JSON.stringify(_canonical(value)).sha256_text()
func _canonical(value):
if value is float and is_equal_approx(value, round(value)):
return int(round(value))
if value is Dictionary:
var result := {}
var keys: Array = value.keys()
keys.sort_custom(func(left, right): return String(left) < String(right))
for key in keys:
result[key] = _canonical(value[key])
return result
if value is Array:
var result: Array = []
for item in value:
result.append(_canonical(item))
return result
return value