Files
gplan-2/game/sim/monte_carlo.gd
T

120 lines
4.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# 战斗期望蒙特卡洛:当前参数下"胜轮数 / 每轮承伤 / 清图次数 / 开格数"的分布实测。
# 政策 = 理性玩家近似:挖最近可挖格(AutoPlayer 单一实现)。
# 运行:Godot_console.exe --headless --path . --script res://sim/monte_carlo.gd
extends SceneTree
const BattleDriver := preload("res://sim/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现)
const N := 30
const SEED0 := 42
func _initialize() -> void:
var db = load("res://core/config_db.gd").new()
db.load_tables("res://data/tables", false)
for L in [1, 2]:
_run_encounter(db, L)
quit(0)
func _run_encounter(db, L: int) -> void:
var rounds_v: Array = [] # 胜利战斗的轮数
var dmg_per_round: Array = [] # 每战斗 每轮承伤
var dmg_total: Array = []
var safe_opened: Array = []
var clears: Array = [] # 每战斗清图(换新板)次数
var buff_grants: Array = [] # attack_up+ac_up 授予数
var losses := 0
var timeouts := 0
for i in range(N):
var seed_val: int = SEED0 + i
var r := _run_battle(db, seed_val, L)
if r.result == "victory":
rounds_v.append(r.rounds)
dmg_total.append(r.hero_dmg)
dmg_per_round.append(float(r.hero_dmg) / float(r.rounds))
safe_opened.append(r.safe_opened)
clears.append(r.board_swaps)
buff_grants.append(r.buffs)
elif r.result == "defeat":
losses += 1
else:
timeouts += 1
print("=== L%d · %d 种子(败 %d / 超时 %d)===" % [L, N, losses, timeouts])
print(" 胜利轮数 均值 %.2f 中位 %d p10 %.1f p90 %.1f 分布 %s" % [_mean(rounds_v), _median(rounds_v), _pct_f(rounds_v, 0.1), _pct_f(rounds_v, 0.9), _hist(rounds_v)])
print(" 每轮承伤 均值 %.2f 中位 %.1f p90 %.1f" % [_mean(dmg_per_round), _median_f(dmg_per_round), _pct_f(dmg_per_round, 0.9)])
print(" 整场承伤 均值 %.1f 中位 %.0f(主角 max_hp=%d)" % [_mean(dmg_total), _median_f(dmg_total), int(db.hero.max_hp)])
print(" 开安全格 均值 %.1f 中位 %d" % [_mean(safe_opened), _median(safe_opened)])
print(" 清图(换板) 均值 %.2f 中位 %d" % [_mean(clears), _median(clears)])
print(" 攻防buff授予 均值 %.2f" % [_mean(buff_grants)])
func _run_battle(db, seed_val: int, L: int) -> Dictionary:
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_val)
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
var out := {"rounds": 0, "result": "", "hero_dmg": 0, "safe_opened": 0, "board_swaps": 0, "buffs": 0}
settlement.battle_finished.connect(func(payload): out.result = payload.get("result", ""))
settlement.board_changed.connect(func(): out.board_swaps += 1)
settlement.step_row.connect(func(row):
if row.type == "damage" and row.source_class == "enemy_action":
out.hero_dmg += int(row.value)
elif row.type == "effect" and row.source_class == "safe_cell" and (row.effect_id == "attack_up" or row.effect_id == "ac_up"):
out.buffs += 1)
settlement.minesweeper.cell_revealed.connect(func(evt):
if evt.kind == "safe" and evt.channel != "initial":
out.safe_opened += 1)
if not settlement.begin_battle({"level_index": L, "node_depth": 0, "hero_hp": int(db.hero.max_hp)}):
return out
var guard_rounds := 40
while out.result == "" and guard_rounds > 0:
guard_rounds -= 1
out.rounds += 1
BattleDriver.drive_round(settlement, func() -> bool: return out.result == "")
if out.result == "":
out.result = "timeout"
return out
func _mean(a: Array) -> float:
if a.is_empty():
return 0.0
var t := 0.0
for v in a:
t += float(v)
return t / a.size()
func _median(a: Array) -> int:
return int(_median_f(a))
func _median_f(a: Array) -> float:
if a.is_empty():
return 0.0
var b := a.duplicate()
b.sort()
return float(b[b.size() / 2])
func _pct_f(a: Array, p: float) -> float:
if a.is_empty():
return 0.0
var b := a.duplicate()
b.sort()
return float(b[mini(b.size() - 1, int(p * b.size()))])
func _hist(a: Array) -> String:
var h := {}
for v in a:
h[v] = int(h.get(v, 0)) + 1
var keys := h.keys()
keys.sort()
var parts: Array = []
for k in keys:
parts.append("%d轮×%d" % [k, h[k]])
return "{" + ", ".join(parts) + "}"