Files
aetherbound-guild/runtime/tests/p10_0/offer_generator_test.gd
T

173 lines
8.6 KiB
GDScript

extends SceneTree
const Content = preload("res://p10_0/content_registry.gd")
const OfferGenerator = preload("res://p10_0/offer_generator.gd")
const SEED_A := "0123456789abcdef0123456789abcdef"
const SEED_B := "fedcba9876543210fedcba9876543210"
var failures: Array[String] = []
var rejected_cases := 0
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
var generator = OfferGenerator.new()
expect(generator.validation().ok, "generator accepts canonical catalog")
verify_opening_offers(generator)
verify_market_boards(generator)
verify_substream_independence(generator)
verify_negative_boundaries(generator)
if failures.is_empty():
print("ABG_P10_0_OFFER_GENERATOR_OK opening=4 selected=2of4 seed_reserved=true markets=24 board_shape=3,4,3 risks=0,1,2 base_only=true one_item=true same_input=byte_identical refresh_owned=true combat_presentation_split=true no_clock_rng=true negative_cases=%d" % rejected_cases)
quit(0)
else:
for failure in failures:
push_error(failure)
quit(1)
func verify_opening_offers(generator) -> void:
var draft := opening_draft(SEED_A)
var first: Dictionary = generator.opening_offers(draft)
var second: Dictionary = OfferGenerator.new().opening_offers(draft)
expect(first.ok and first == second, "same reserved draft reproduces byte-identical materialization")
expect(first.offers.size() == 4 and String(first.materialization.materialization_hash).length() == 64, "opening materializes four complete rows under one hash")
var offer_ids := {}
var recruit_ids := {}
var item_ids := {}
for index in range(first.offers.size()):
var offer: Dictionary = first.offers[index]
expect(int(offer.offer_index) == index, "opening offer index %d is retained" % index)
expect(String(offer.source_offer_id).length() == 32 and String(offer.recruit_id).length() == 32 and String(offer.generation_key).length() == 32, "opening offer %d has low128 stable identities" % index)
expect(not offer_ids.has(offer.source_offer_id) and not recruit_ids.has(offer.recruit_id), "opening offer %d identities are unique" % index)
expect(String(offer.profession_id).begins_with("PF-B") and not String(offer.profession_id).begins_with("PF-A"), "opening offer %d uses a base Profession" % index)
expect(String(offer.trait_id).begins_with("TR-") and String(offer.starting_equipment_id).begins_with("EQ-"), "opening offer %d has one Trait and one equipment item" % index)
expect(String(offer.canonical_output_hash).length() == 64 and offer.current_hp == offer.max_hp and offer.level >= 1, "opening offer %d is a complete Recruit record" % index)
offer_ids[offer.source_offer_id] = true
recruit_ids[offer.recruit_id] = true
item_ids[offer.starting_equipment_id] = true
expect(item_ids.size() == 4, "opening avoids duplicate starting equipment while alternatives exist")
func verify_market_boards(generator) -> void:
for contract_id in ["CTR-R1-01", "CTR-R1-02"]:
for market_index in range(1, 13):
var result: Dictionary = generator.market_board(SEED_A, contract_id, market_index)
expect(result.ok, "%s Market %d generates" % [contract_id, market_index])
var board: Dictionary = result.board
expect(board.recruit_offers.size() == 3 and board.equipment_offers.size() == 4 and board.battle_offers.size() == 3, "%s Market %d has exact 3/4/3 board" % [contract_id, market_index])
expect(board.battle_offers.map(func(offer): return int(offer.risk)) == [0, 1, 2], "%s Market %d has ordered risks 0/1/2" % [contract_id, market_index])
expect(board.battle_offers.map(func(offer): return int(offer.wave_count)) == [2, 3, 4], "%s Market %d previews 2/3/4 fixed waves" % [contract_id, market_index])
expect(board.recruit_offers.all(func(offer): return String(offer.profession_id).begins_with("PF-B") and String(offer.starting_equipment_id).begins_with("EQ-")), "%s Market %d recruits are base-only with one item" % [contract_id, market_index])
expect(String(board.board_hash).length() == 64, "%s Market %d has stable board hash" % [contract_id, market_index])
var repeated: Dictionary = generator.market_board(SEED_A, contract_id, market_index)
expect(result == repeated, "%s Market %d is byte-identical on repeat" % [contract_id, market_index])
var baseline: Dictionary = generator.market_board(SEED_A, "CTR-R1-01", 4, 0).board
var refreshed: Dictionary = generator.market_board(SEED_A, "CTR-R1-01", 4, 1).board
expect(baseline.recruit_offers != refreshed.recruit_offers and baseline.equipment_offers != refreshed.equipment_offers, "refresh changes Recruit and equipment draws")
expect(baseline.battle_offers == refreshed.battle_offers, "refresh cannot reroll battle choices")
var next_market: Dictionary = generator.market_board(SEED_A, "CTR-R1-01", 5, 0).board
expect(baseline.recruit_offers != next_market.recruit_offers and baseline.equipment_offers != next_market.equipment_offers and baseline.battle_offers != next_market.battle_offers, "Market index changes all Market-owned draws")
var changed_seed: Dictionary = generator.market_board(SEED_B, "CTR-R1-01", 4, 0).board
expect(baseline.recruit_offers != changed_seed.recruit_offers and baseline.equipment_offers != changed_seed.equipment_offers and baseline.battle_offers != changed_seed.battle_offers, "Run seed changes all seeded draws")
func verify_substream_independence(generator) -> void:
var stable: Dictionary = generator.opening_offers(opening_draft(SEED_A), 0)
var presentation_changed: Dictionary = generator.opening_offers(opening_draft(SEED_A), 1)
for index in range(4):
var first: Dictionary = stable.offers[index]
var changed: Dictionary = presentation_changed.offers[index]
expect(combat_projection(first) == combat_projection(changed), "presentation revision leaves opening combat offer %d unchanged" % index)
expect(presentation_projection(first) != presentation_projection(changed), "presentation revision changes only visual/name offer %d" % index)
func verify_negative_boundaries(generator) -> void:
var unreserved := opening_draft(SEED_A)
unreserved.seed_reservation_applied = false
expect_rejected(generator.opening_offers(unreserved), "unapplied seed reservation", "seed_reservation_not_applied")
var wrong_revision := opening_draft(SEED_A)
wrong_revision.content_revision = "stale"
expect_rejected(generator.opening_offers(wrong_revision), "stale content revision", "content_revision_mismatch")
expect_rejected(generator.market_board("bad-seed", "CTR-R1-01", 1), "malformed Run seed", "invalid_run_seed")
expect_rejected(generator.market_board(SEED_A, "CTR-UNKNOWN", 1), "unknown Contract", "unknown_contract")
expect_rejected(generator.market_board(SEED_A, "CTR-R1-01", 13), "Market outside Contract", "invalid_market_index")
var malformed_catalog := Content.snapshot()
malformed_catalog.equipment[0].id = "EQ-999"
var rejected_generator = OfferGenerator.new(malformed_catalog)
expect_rejected(rejected_generator.market_board(SEED_A, "CTR-R1-01", 1), "malformed content authority", "invalid_catalog")
func opening_draft(seed: String) -> Dictionary:
return {
"draft_id": "DRAFT-RG01-001",
"run_seed": seed,
"contract_id": "CTR-R1-01",
"difficulty": "standard",
"ruleset": "p10.0-contract-r1",
"content_revision": Content.SOURCE_REVISION,
"region_floor": 1,
"seed_reservation_applied": true,
"read_back_validated": true,
"role_weights": {
"front-hold": 3,
"single-pressure": 1,
"reach": 2,
"reorder": 1,
"projectile": 2,
"area": 1,
"restore": 2,
"enable": 1,
"construct": 1,
"impair": 1,
"tempo": 1,
"supply": 1,
},
}
func combat_projection(recruit: Dictionary) -> Dictionary:
return {
"recruit_id": recruit.recruit_id,
"generation_key": recruit.generation_key,
"profession_id": recruit.profession_id,
"trait_id": recruit.trait_id,
"attributes": recruit.attribute_rolls,
"quality": recruit.quality_band,
"level": recruit.level,
"equipment": recruit.starting_equipment_id,
"max_hp": recruit.max_hp,
"cost_tier": recruit.shop_cost_tier,
}
func presentation_projection(recruit: Dictionary) -> Dictionary:
return {
"name": recruit.display_name_tokens,
"appearance": recruit.appearance_seed,
"body": recruit.body_frame,
"face": recruit.face_shape,
"hair": recruit.hair_shape,
"garment": recruit.garment_cut,
}
func expect_rejected(result: Dictionary, label: String, expected_code: String) -> void:
var accepted := not bool(result.get("ok", false)) and String(result.get("code", "")) == expected_code
expect(accepted, "%s rejects with %s; got %s" % [label, expected_code, str(result)])
if accepted:
rejected_cases += 1
func expect(condition: bool, message: String) -> void:
if not condition:
failures.append("P10.0 offer generator: %s" % message)