173 lines
8.6 KiB
GDScript
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)
|