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

139 lines
5.0 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
# Date: 2026-09-11
# 战斗期望蒙特卡洛:当前参数下"胜轮数 / 每轮承伤 / 清图次数 / 开格数"的分布实测。
# 政策 = 理性玩家近似:挖最近可挖格(AutoPlayer 单一实现——O4 收口;道具有效期政策随可选类删除退役 2026-09-15)。
# 运行:Godot_console.exe --headless --path . --script res://smoke/monte_carlo.gd
extends SceneTree
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口(批注 2026-09-15:原文件缺失 preload——HEAD 起即解析失败,本批顺手修复)
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(rounds_v, 0.1), _pct(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
var start_round: int = settlement.round_index
var minesweeper = settlement.minesweeper
var guard := 400
while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "":
guard -= 1
var plan := AutoPlayer.choose_dig(minesweeper)
if plan.is_empty():
break
if plan.hero_cell != minesweeper.hero_pos():
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
break
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "":
minesweeper.settle_batch([{"action": "stop"}])
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(a: Array, p: float) -> float:
return _pct_f(a, p)
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) + "}"