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

416 lines
16 KiB
GDScript

class_name P103RunState
extends "res://p10_2/run_state.gd"
const P102State = preload("res://p10_2/run_state.gd")
const P103_STATE_KIND := "p103_siltwake_deep_channels"
const P103_RULESET_VERSION := "p10.3-siltwake-deep-channels-r1"
const ARTIFACT_IDS := ["AR-011", "AR-012", "AR-015"]
func initialize_from_p102(p102_snapshot: Dictionary, market_board: Dictionary) -> bool:
var probe := P102State.new()
if not probe.restore_snapshot(p102_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_5"
or int(previous_run.market_index) != 5
or not bool(previous_run.reed_ferry_restored)
or String(market_board.get("run_seed", "")) != String(previous_run.run_seed)
or int(market_board.get("market_index", -1)) != 5
or int(market_board.get("refresh_index", -1)) != 0
):
return false
state_kind = P103_STATE_KIND
ruleset = P103_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.legacy_operation_receipts).duplicate(true)
for operation_id in source.operation_receipts:
if legacy_operation_receipts.has(operation_id):
return false
legacy_operation_receipts[operation_id] = Dictionary(source.operation_receipts[operation_id]).duplicate(true)
legacy_entry_commits = Dictionary(source.legacy_entry_commits).duplicate(true)
battle_checkpoints = {}
current_run = previous_run.duplicate(true)
current_run.phase = "market"
current_run.market_board = market_board.duplicate(true)
current_run.refresh_count = 0
current_run.available_recruit_offer_ids = _offer_ids(market_board.recruit_offers, "source_offer_id")
current_run.available_equipment_offer_ids = _offer_ids(market_board.equipment_offers, "offer_id")
current_run.available_battle_offer_ids = _offer_ids(market_board.battle_offers, "offer_id")
current_run.locked_offer_ids = []
current_run.selected_route_id = ""
current_run.pending_encounter = {}
current_run.pre_battle_checkpoint_id = ""
current_run.recovery_required = false
current_run.recovery_cost = 0
current_run.active_artifact_ids = []
current_run.artifact_rule_state = {}
current_run.artifact_draft = {}
current_run.promotion_draft = {}
for recruit_id in current_run.company:
var record: Dictionary = current_run.recruit_records[recruit_id]
var origin := String(record.get("base_profession_id", record.profession_id))
record["origin_profession_id"] = origin
record["base_profession_id"] = origin
record["promotion_state"] = "base"
record["promotion_history"] = []
current_run.recruit_records[recruit_id] = record
return invariants_hold()
func invariants_hold() -> bool:
if state_kind != P103_STATE_KIND or ruleset != P103_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 _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",
"active_artifact_ids", "artifact_rule_state", "artifact_draft",
"promotion_draft",
]
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",
"artifact_rule_state", "artifact_draft", "promotion_draft",
]:
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", "active_artifact_ids",
]:
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
var phase := String(current_run.phase)
if String(current_run.status) != "active" or phase not in ["market", "prepared", "recovery", "market_8"]:
return false
var market_index := int(current_run.market_index)
var cleared_rounds := int(current_run.cleared_rounds)
var expected_capacity: int = {5: 6, 6: 6, 7: 7, 8: 7}.get(market_index, -1)
if market_index < 5 or market_index > 8 or int(current_run.round_index) != market_index:
return false
if cleared_rounds != market_index - 1 or int(current_run.line_capacity) != expected_capacity:
return false
if int(current_run.coin) < 0 or int(current_run.replacement_credit) < 0 or int(current_run.refresh_count) < 0:
return false
if not bool(current_run.reed_ferry_restored):
return false
if current_run.route_history.size() != cleared_rounds or current_run.first_crossing_handoff.is_empty():
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 not _board_invariants_hold() or not _company_invariants_hold():
return false
if not _artifact_invariants_hold() or not _promotion_invariants_hold():
return false
if phase == "market":
return (
market_index >= 5
and market_index <= 7
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 >= 5
and market_index <= 7
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 >= 5
and market_index <= 7
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 == 8
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) == 8:
for key in [
"handoff_kind", "board_hash", "run_seed", "contract_id", "region_id",
"market_index", "cleared_rounds", "line_capacity",
"source_settlement_hash", "active_artifact_ids",
"promotion_state_hash",
]:
if not board.has(key):
return false
if (
String(board.handoff_kind) != "p10.3.market_8_continuation"
or String(board.run_seed) != String(current_run.run_seed)
or String(board.contract_id) != "CTR-R2-01"
or String(board.region_id) != "RG-02"
or int(board.market_index) != 8
or int(board.cleared_rounds) != 7
or int(board.line_capacity) != 7
or Array(board.active_artifact_ids) != Array(current_run.active_artifact_ids)
or not _is_hex(String(board.source_settlement_hash), 64)
or String(board.promotion_state_hash) != _hash(_promotion_projection())
):
return false
var handoff := board.duplicate(true)
var handoff_hash := String(handoff.board_hash)
handoff.erase("board_hash")
return (
handoff_hash == _hash(handoff)
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()
)
var expected_offer_level: int = {
5: 2,
6: 2,
7: 3,
}.get(int(current_run.market_index), -1)
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", "growth_disclosure",
"generation_authority",
]:
if not board.has(key):
return false
if (
String(board.run_seed) != String(current_run.run_seed)
or String(board.contract_id) != "CTR-R2-01"
or String(board.region_id) != "RG-02"
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)
or int(board.cleared_rounds_before) != int(current_run.cleared_rounds)
or int(board.offer_level) != expected_offer_level
):
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 candidate := board.duplicate(true)
var stored_hash := String(candidate.board_hash)
candidate.erase("board_hash")
if stored_hash != _hash(candidate):
return false
var expected_milestone: String = {
5: "linked_positions",
6: "artifact_after_victory",
7: "first_promotion",
}.get(int(current_run.market_index), "")
if (
not board.growth_disclosure is Dictionary
or String(board.growth_disclosure.get("milestone", "")) != expected_milestone
):
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")
var lock_authority: Array = recruit_ids + equipment_ids
return (
_subset(current_run.available_recruit_offer_ids, recruit_ids)
and _subset(current_run.available_equipment_offer_ids, equipment_ids)
and _subset(current_run.available_battle_offer_ids, battle_ids)
and _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:
if not super._company_invariants_hold():
return false
for recruit_id in current_run.company:
var record: Dictionary = current_run.recruit_records[recruit_id]
var origin := String(record.get("origin_profession_id", ""))
var current := String(record.get("profession_id", ""))
var promotion_state := String(record.get("promotion_state", ""))
if not origin.begins_with("PF-B") or String(record.get("base_profession_id", "")) != origin:
return false
if not record.get("promotion_history", []) is Array:
return false
if promotion_state == "base" and (current != origin or not record.promotion_history.is_empty()):
return false
if promotion_state == "regular_advanced" and (not current.begins_with("PF-A") or record.promotion_history.size() != 1):
return false
if promotion_state not in ["base", "regular_advanced"]:
return false
return true
func _artifact_invariants_hold() -> bool:
var active: Array = current_run.active_artifact_ids
var rules: Dictionary = current_run.artifact_rule_state
if not _unique(active) or active.size() > 1 or rules.size() != active.size():
return false
for artifact_id in active:
if artifact_id not in ARTIFACT_IDS or not rules.has(artifact_id):
return false
if (
String(rules[artifact_id].get("artifact_id", "")) != artifact_id
or int(rules[artifact_id].get("activated_market_index", -1)) != 7
or not rules[artifact_id].get("rule", {}) is Dictionary
or rules[artifact_id].get("rule", {}).is_empty()
):
return false
var draft: Dictionary = current_run.artifact_draft
if int(current_run.market_index) < 7:
return draft.is_empty() and active.is_empty()
if draft.is_empty():
return false
var status := String(draft.get("status", ""))
if (
String(draft.get("draft_id", "")) != "AD-R2-M07-01"
or String(draft.get("run_seed", "")) != String(current_run.run_seed)
or draft.get("offer_ids", []) != ARTIFACT_IDS
or int(draft.get("created_after_round", -1)) != 6
or int(draft.get("apply_from_market", -1)) != 7
or String(draft.get("draft_hash", "")) != _artifact_draft_hash(draft)
or status not in ["available", "deferred", "selected"]
):
return false
var selected := String(draft.get("selected_artifact_id", ""))
if status == "selected":
return active == [selected] and selected in ARTIFACT_IDS
return selected.is_empty() and active.is_empty()
func _promotion_invariants_hold() -> bool:
var draft: Dictionary = current_run.promotion_draft
if draft.is_empty():
return true
if int(current_run.market_index) not in [7, 8]:
return false
var recruit_id := String(draft.get("recruit_id", ""))
var options = draft.get("option_ids", [])
var status := String(draft.get("status", ""))
var record: Dictionary = current_run.recruit_records.get(recruit_id, {})
var current_profession := String(draft.get("current_profession_id", ""))
if (
recruit_id not in current_run.company
or record.is_empty()
or String(draft.get("origin_profession_id", "")) != String(record.get("origin_profession_id", ""))
or current_profession != String(record.get("origin_profession_id", ""))
or not options is Array
or options != _expected_descendants(current_profession)
or String(draft.get("draft_hash", "")) != _promotion_draft_hash(draft)
or status not in ["available", "deferred", "selected"]
):
return false
var chosen := String(draft.get("chosen_profession_id", ""))
if status == "selected":
return (
chosen in options
and String(record.profession_id) == chosen
and String(record.promotion_state) == "regular_advanced"
)
return (
chosen.is_empty()
and String(record.profession_id) == current_profession
and String(record.promotion_state) == "base"
)
func _artifact_draft_hash(draft: Dictionary) -> String:
var projection := draft.duplicate(true)
for key in ["draft_hash", "status", "selected_artifact_id"]:
projection.erase(key)
return _hash(projection)
func _promotion_draft_hash(draft: Dictionary) -> String:
var projection := draft.duplicate(true)
for key in ["draft_hash", "status", "chosen_profession_id"]:
projection.erase(key)
return _hash(projection)
func _promotion_projection() -> Dictionary:
var result := {}
for recruit_id in current_run.company:
var record: Dictionary = current_run.recruit_records[recruit_id]
if String(record.get("promotion_state", "")) == "regular_advanced":
result[String(recruit_id)] = {
"origin_profession_id": String(record.origin_profession_id),
"profession_id": String(record.profession_id),
}
return result
func _expected_descendants(base_profession_id: String) -> Array[String]:
if not base_profession_id.begins_with("PF-B"):
return []
var base_index := int(base_profession_id.trim_prefix("PF-B"))
if base_index < 1 or base_index > 12:
return []
return [
"PF-A%02d" % (2 * base_index - 1),
"PF-A%02d" % (2 * base_index),
]