extends Node ## Golden references for the C# port: interprets Assets/Luneraria/Tests/Core/Golden/scenarios.json ## with the Godot rules (luneraria/g3, 1b540f2) and writes the domain state after every ## "snapshot" operation plus the result of every command (Golden/golden.json). The C# core ## (GoldenSuite) runs the same operations and compares its own snapshots field by field. ## Run: sh tools/godot-golden/run_godot.sh golden_scenarios --in= --out= [--only=id,...] var state: LuneMatch var snapshots: Array = [] var results: Array = [] func _ready() -> void: var source: String = "" var out: String = "/tmp/lune_golden.json" var only: PackedStringArray = [] for arg: String in OS.get_cmdline_user_args(): if arg.begins_with("--in="): source = arg.substr(5) elif arg.begins_with("--out="): out = arg.substr(6) elif arg.begins_with("--only="): only = arg.substr(7).split(",") var spec: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(source)) var scenarios: Dictionary = {} for scenario: Dictionary in spec.scenarios: if not only.is_empty() and not only.has(scenario.id): continue var started: int = Time.get_ticks_msec() snapshots = [] results = [] for operation: Dictionary in scenario.ops: _apply(operation) scenarios[scenario.id] = {"title": scenario.title, "snapshots": snapshots, "results": results} print("GOLDEN %s: %d snapshots, %d results, %d ms" % [scenario.id, snapshots.size(), results.size(), Time.get_ticks_msec() - started]) var file := FileAccess.open(out, FileAccess.WRITE) file.store_string(JSON.stringify({"engine": Engine.get_version_info().string, "reference": "luneraria/g3 1b540f2", "scenarios": scenarios}, "", false, true)) file.close() print("GOLDEN_DONE ", out) get_tree().quit() # ---------------------------------------------------------------- operations func _apply(op: Dictionary) -> void: match str(op.op): "setup": state = LuneMatch.new() var options: Dictionary = {"seed": int(op.seed), "human_slot": int(op.get("human", -1))} if op.has("bunds"): options.bunds = op.bunds if op.has("profiles"): options.profiles = op.profiles state.setup(options) "building": state.building_hp[str(op.id)] = float(op.hp) "buildings_full": for definition: Dictionary in state.map.buildings: state.building_hp[definition.id] = float(definition.max_hp) "round": state.round_number = int(op.value) "gold": state.players[int(op.slot)].draft.gold = int(state.players[int(op.slot)].draft.gold) + int(op.add) "gold_all": for player: Dictionary in state.players: player.draft.gold = int(player.draft.gold) + int(op.add) "cmd": var command: Dictionary = _resolve(int(op.slot), op.command) var result: Dictionary = state.submit(int(op.slot), command) results.append(["cmd", int(op.slot), str(command.type), bool(result.ok), str(result.message)]) "bots": for slot: int in range(6): LuneBot.plan(state, slot, str(op.get("profile", ""))) "begin": results.append(["begin", -1, "", state.begin_combat(), ""]) "steps": for i: int in range(int(op.n)): if state.phase != LuneMatch.Phase.COMBAT: break state.step_combat() "finish": state.finish_combat() "unit_set": var select: Dictionary = op.select for unit: LuneUnit in state.combat.units: if select.has("kind") and unit.kind != str(select.kind): continue if select.has("zone") and unit.zone != str(select.zone): continue if select.has("team") and unit.team != int(select.team): continue if op.has("hp"): unit.hp = float(op.hp) "settle_boss": results.append(["settle_boss", -1, "", true, state._settle_boss()]) "expedition": var slot: int = int(op.slot) state.players[slot].draft.gold = int(state.players[slot].draft.gold) + 6000 var ids: Array[String] = state.base_ids(state.players[slot].bund) for type_index: Variant in op.types: _record(slot, {"type": "buy", "type_id": ids[int(type_index)]}) var entry: Dictionary = state.players[slot].draft.roster[-1] _record(slot, {"type": "upgrade", "uid": entry.uid}) _record(slot, {"type": "assign", "uid": entry.uid, "lane": "grotto"}) if op.has("hero"): _record(slot, {"type": "hero_buy", "hero_id": str(op.hero)}) _record(slot, {"type": "assign", "uid": state.players[slot].draft.hero.uid, "lane": "grotto"}) "play": for i: int in range(int(op.rounds)): if state.phase == LuneMatch.Phase.OVER: break if bool(op.get("bots", true)): for slot: int in range(6): LuneBot.plan(state, slot, str(op.get("profile", ""))) var fought: int = state.round_number state.begin_combat() state.finish_combat() _snapshot({"label": "round-%d" % fought}) "snapshot": _snapshot(op) _: push_error("unknown op " + str(op.op)) func _record(slot: int, command: Dictionary) -> void: var result: Dictionary = state.submit(slot, command) results.append(["cmd", slot, str(command.type), bool(result.ok), str(result.message)]) ## Uid references: $last:, $roster::, $hero:, $booking::. func _resolve(slot: int, command: Dictionary) -> Dictionary: var copy: Dictionary = command.duplicate(true) if copy.has("uid") and str(copy.uid).begins_with("$"): var parts: PackedStringArray = str(copy.uid).substr(1).split(":") var owner: Dictionary = state.players[int(parts[1])] match parts[0]: "last": copy.uid = str(owner.draft.roster[-1].uid) "roster": copy.uid = str(owner.draft.roster[int(parts[2])].uid) "hero": copy.uid = str(owner.draft.hero.uid) "booking": copy.uid = str(owner.draft.new_bookings[int(parts[2])].uid) if copy.has("tier"): copy.tier = int(copy.tier) if copy.has("choice"): copy.choice = int(copy.choice) return copy # ---------------------------------------------------------------- snapshot func _snapshot(op: Dictionary) -> void: var players: Array = [] for player: Dictionary in state.players: players.append({"gold": int(player.gold), "draft_gold": int(player.draft.gold), "ready": bool(player.ready), "earned": int(player.earned), "spent": int(player.spent), "next_seq": int(player.next_seq), "roster": _roster(player.roster, false), "hero": _hero(player.hero), "bookings": player.bookings.map(func(b: Dictionary) -> String: return "%s|%s|%s|%d|%d" % [b.uid, b.champion_id, b.lane, int(b.cost), int(b.get("deploy_round", 0))]), "new_bookings": (player.draft.new_bookings as Array).map(func(b: Dictionary) -> String: return "%s|%s|%s|%d" % [b.uid, b.champion_id, b.lane, int(b.cost)]), "draft_roster": _roster(player.draft.roster, true), "draft_hero": _hero(player.draft.hero), "boss_lane": str(player.draft.boss_lane), "undo": (player.undo as Array).size()}) var claim: Dictionary = state.boss.get("claim", {}) var snap: Dictionary = {"label": str(op.label), "phase": state.phase, "round": state.round_number, "winner": state.winner, "end_note": state.end_note, "players": players, "buildings": state.map.buildings.map(func(d: Dictionary) -> float: return float(state.building_hp[d.id])), "boss": {"next": int(state.boss.next_available_round), "settled": int(state.boss.get("settled_round", 0)), "claim": "" if claim.is_empty() else "%d|%d|%d|%d|%s" % [int(claim.team), int(claim.won_round), int(claim.deploy_round), int(claim.chooser_slot), claim.lane]}, "boss_log": state.boss_log.map(func(e: Dictionary) -> String: return "%d|%s|%d|%s|%d" % [int(e.round), e.event, int(e.get("team", -1)), str(e.get("lane", "")), int(e.get("chooser", -1))]), "announcements": state.announcements().map(func(a: Dictionary) -> String: return "%s|%d|%d|%s|%s|%d" % [a.kind, int(a.team), int(a.slot), a.lane, str(a.get("champion_id", "")), int(a.get("bounty", 0))]), "settlement": _settlement(), "combat": _combat()} if bool(op.get("units", false)) and state.combat != null: snap.units = state.combat.units.map(_unit) if bool(op.get("events", false)) and state.combat != null: snap.events = state.combat.events.map(func(e: Dictionary) -> Array: return [int(e.serial), int(e.step), float(e.time), int(e.team), str(e.lane), str(e.zone), str(e.kind), int(e.unit_id), str(e.def_id), int(e.bounty), str(e.get("building_id", "")), int(e.xp), e.xp_heroes]) if bool(op.get("wave", false)): snap.wave = LuneWaves.build(state.balance, state.round_number).map(func(m: Dictionary) -> Array: return [str(m.id), float(m.max_hp), float(m.damage), int(m.bounty)]) snapshots.append(snap) func _roster(roster: Array, with_fresh: bool) -> Array: return roster.map(func(e: Dictionary) -> String: return "%s|%s|%s|%d%s" % [e.uid, e.type_id, e.lane, int(e.invested), ("|%s" % str(bool(e.get("fresh", false))).to_lower()) if with_fresh else ""]) func _hero(hero: Dictionary) -> String: if hero.is_empty(): return "" return "%s|%s|%d|%s|%s|%s|%d|%s|%s" % [hero.uid, hero.hero_id, int(hero.xp), hero.lane, "+".join(hero.items), "+".join(hero.talents), int(hero.invested), str(bool(hero.get("fresh", false))).to_lower(), "+".join(hero.get("fresh_items", []))] func _settlement() -> Variant: var s: Dictionary = state.last_settlement if s.is_empty(): return null return {"round": int(s.round), "gains": (s.gains as Array).map(func(g: int) -> int: return g), "income": int(s.income), "lost": s.buildings_lost, "end_reason": str(s.end_reason), "combat_time": float(s.combat_time), "boss": str(s.boss), "lane_kills": s.lane_kills, "xp": s.xp} func _combat() -> Variant: var c: LuneCombat = state.combat if c == null: return null var boss: String = "" if c.grotto_boss != null: boss = "%s|%s|%s" % [str(c.grotto_boss.alive).to_lower(), str(c.grotto_boss.dormant).to_lower(), str(c.grotto_boss.id)] return {"round": c.round_number, "steps": c.steps, "elapsed": c.elapsed, "finished": c.finished, "end_reason": c.end_reason, "destroyed": c.destroyed_cores.duplicate(), "grotto": "%s|%d|%d|%s" % [c.grotto_outcome, c.grotto_claim_team, c.grotto_active_team, str(c.grotto_resolved).to_lower()], "grotto_boss": boss, "grotto_boss_hp": c.grotto_boss.hp if c.grotto_boss != null else 0.0, "events": c.events.size(), "mobile": [c.mobile_count(0), c.mobile_count(1), c.mobile_count(2)]} func _unit(unit: LuneUnit) -> Dictionary: var effects: Array = [] for family: String in unit.effects: var effect: Dictionary = unit.effects[family] effects.append([family, str(effect.kind), float(effect.amount), float(effect.remaining)]) var dots: Array = [] for family: String in unit.dots: var dot: Dictionary = unit.dots[family] dots.append([family, float(dot.damage), float(dot.remaining), float(dot.next_tick), int(dot.attack_type)]) return {"id": unit.id, "kind": unit.kind, "team": unit.team, "lane": unit.lane, "zone": unit.zone, "slot": unit.owner_slot, "uid": unit.persistent_uid, "def": str(unit.def.get("id", "")), "alive": unit.alive, "hp": unit.hp, "max_hp": unit.max_hp, "shield": unit.shield, "x": unit.position.x, "y": unit.position.y, "facing": unit.facing, "progress": unit.progress, "target": unit.target.id if unit.target != null else -1, "hits": unit.hit_count, "reached": unit.reached_base, "dormant": unit.dormant, "effects": effects, "dots": dots}