
杀戮尖塔2问号房一共可能出现4种房间类型:怪物、宝箱、商店、事件,根据反编译的源码分析,4种房间类型中怪物、宝箱、商店有一个固定的基础概率,剩下的情况出现事件。并且遵循以下规律:
怪物、宝箱、商店的基础概率为10%,2%,3%
怪物、宝箱、商店被问号房开出后,该类型概率重置为基础概率,否则累加一次基础概率
采用轮盘赌的随机数检测,按照怪物->宝箱->商店的顺序判定,怪物、宝箱、商店的实际概率不会归一化,这意味着如果三者的当前概率之和大于1,顺序靠后的类型出现概率会被截断
进入新一层后会重置基础概率
参考:# 【杀戮尖塔2】问号出怪概率真的有变大吗?一个视频从源码角度讲清塔2问号机制!
那么,我们自然想知道:
当前局面下问号房出现各个事件的概率有多大
长期来看问号房出现各个事件的占比有多少
先说结论,长期来看各个事件的占比:
怪物: 27.320808%
宝箱: 11.704681%
商店: 14.441581%
事件: 46.532930%
怪物(Monster)、宝箱(Treasure)、商店(Shop)的基础概率:
怪物、宝箱、商店、事件(Event)的当前概率:
当前连续未出现怪物、宝箱、商店的问号房次数:
约束
求任意状态下的当前概率,以及各事件的平稳分布
首先我们要表示当前概率,当前出现怪物房的概率和连续未出现怪物的次数有关,并且不会超过1:
同理,宝箱的概率也如此,但是有个额外限制是宝箱和怪物的概率和也不能超过1:
同理,得到商店和事件的概率:
基础概率是固定死的,只需要知道连续未出现怪物、宝箱、商店的问号房次数: 就可以唯一地表示当前局面的概率了。
观察可以发现,由于保底机制的存在,出现概率随着连续未出现次数单调增加,并且最终达到1,因此某一种类型连续未出现次数存在最大值,记怪物、宝箱、商店连续未出现次数的最大值为M,T,S,则:
,举例来说,怪物房基础概率0.1,那么问号房最多连续不出现怪物的次数是
,因为连续9次不出现怪物后,第10次出现怪物的概率就变成1了。
因此状态空间定义为三元组 ,总状态数为
,每个状态节点有4条边转移到新的状态
若当前状态为,根据各状态转移概率
:
以的概率转移为
以的概率转移为
以的概率转移为
以的概率转移为
我们带入官方的基础概率数值:
用简单的Python脚本计算状态转移矩阵和平稳分布,计算结果和模拟1,000,000实验结果比对:
import math
import itertools
import numpy as np
# ============================================================
# Step 1. 定义基础概率
# ============================================================
# 官方基础概率:
# 怪物 Monster = 10%
# 宝箱 Treasure = 2%
# 商店 Shop = 3%
BASE_PROBS = (0.10, 0.02, 0.03)
ROOM_NAMES = ["怪物", "宝箱", "商店", "事件"]
# ============================================================
# Step 2. 计算当前状态下的实际房间概率
# Step 3. 构造有限状态空间和状态转移矩阵
# Step 4. 计算平稳分布
# Step 5. 计算从新一层开始,前 N 次问号房的平均期望
# Step 6. 蒙特卡洛模拟验证
# ============================================================
# ...
# ============================================================
# Step 7. 主程序
# ============================================================
def main():
# 构建马尔可夫链
states, state_to_idx, trans_mat, room_probs = build_markov_chain(BASE_PROBS)
# 计算平稳分布
stationary = calc_stationary_distribution(trans_mat)
# 长期来看,每类房间的出现概率
long_run_probs = stationary @ room_probs
print("状态总数:", len(states))
print("\n长期平稳分布:")
for name, p in zip(ROOM_NAMES, long_run_probs):
print(f"{name}: {p:.6%}")
print("\n前 1~10 次问号房平均期望:")
print("| 问号房次数 | 怪物 | 宝箱 | 商店 | 事件 |")
print("|---:|---:|---:|---:|---:|")
first_n_result = calc_first_n_average_probs(BASE_PROBS, max_n=10)
for n, probs in first_n_result:
print(
f"| {n} | "
f"{probs[0]:.4%} | "
f"{probs[1]:.4%} | "
f"{probs[2]:.4%} | "
f"{probs[3]:.4%} |"
)
# 模拟 1,000,000 次进行验证
sim_probs = simulate(BASE_PROBS, num_trials=1_000_000, seed=42)
print("\n模拟结果:")
for name, p in zip(ROOM_NAMES, sim_probs):
print(f"{name}: {p:.6%}")
if __name__ == "__main__":
main()
# ============================================================
# 运行结果示例
# ============================================================
#
# 状态总数: 17000
#
# 长期平稳分布:
# 怪物: 27.320808%
# 宝箱: 11.704681%
# 商店: 14.441581%
# 事件: 46.532930%
#
# 前 1~10 次问号房平均期望:
# | 问号房次数 | 怪物 | 宝箱 | 商店 | 事件 |
# |---:|---:|---:|---:|---:|
# | 1 | 10.0000% | 2.0000% | 3.0000% | 85.0000% |
# | 2 | 14.5000% | 2.9800% | 4.4550% | 78.0650% |
# | 3 | 18.1000% | 3.9208% | 5.8227% | 72.1565% |
# | 4 | 20.6425% | 4.8053% | 7.0680% | 67.4842% |
# | 5 | 22.2382% | 5.6198% | 8.1661% | 63.9759% |
# | 6 | 23.1702% | 6.3544% | 9.1045% | 61.3710% |
# | 7 | 23.7394% | 7.0037% | 9.8804% | 59.3765% |
# | 8 | 24.1508% | 7.5666% | 10.5072% | 57.7755% |
# | 9 | 24.4916% | 8.0459% | 11.0037% | 56.4588% |
# | 10 | 24.7795% | 8.4479% | 11.3918% | 55.3807% |
#
# 模拟结果:
# 怪物: 27.313300%
# 宝箱: 11.687100%
# 商店: 14.454000%
# 事件: 46.545600%
可以看到模拟结果符合我们的理论计算。也就是说,从长期的角度看,如果我们无限的进问号房,遇到怪物、宝箱、商店和事件的概率分别为:27.3%, 11.7%, 14.4%, 46.5%。当然我们不可能无限的进,这里根据单层进入1~10次问号房各事件的平均期望来看:问号走的越多,遇到怪物的可能性越高,但是宝箱和商店的可能性也越高
数学问题解决了,有什么用呢?
可以开发一个工具,直接计算当前局面问号房出现各事件出现的概率,辅助决策
可以从长期角度看统计结论,辅助大局观上的决策
import math
import itertools
import numpy as np
# ============================================================
# Step 1. 定义基础概率
# ============================================================
# 官方基础概率:
# 怪物 Monster = 10%
# 宝箱 Treasure = 2%
# 商店 Shop = 3%
BASE_PROBS = (0.10, 0.02, 0.03)
ROOM_NAMES = ["怪物", "宝箱", "商店", "事件"]
# ============================================================
# Step 2. 计算当前状态下的实际房间概率
# ============================================================
def calc_current_probs(state, base_probs):
"""
根据当前状态 (m, t, s) 计算问号房实际出现概率。
state:
m: 连续未出现怪物的次数
t: 连续未出现宝箱的次数
s: 连续未出现商店的次数
注意:
游戏使用顺序轮盘赌:
怪物 -> 宝箱 -> 商店 -> 事件
因此:
怪物概率不受后续类型影响;
宝箱概率会被怪物概率截断;
商店概率会被怪物 + 宝箱概率截断;
事件概率是剩余概率。
"""
m, t, s = state
p_m_base, p_t_base, p_s_base = base_probs
# 怪物概率:连续没出怪越久,概率越高,但最多为 1
p_m = min((m + 1) * p_m_base, 1.0)
# 宝箱概率:自身概率最多为 (t + 1) * 基础概率,
# 但因为怪物先判定,所以不能超过剩余概率 1 - p_m
p_t = min((t + 1) * p_t_base, max(0.0, 1.0 - p_m))
# 商店概率:同理,会被怪物和宝箱截断
p_s = min((s + 1) * p_s_base, max(0.0, 1.0 - p_m - p_t))
# 事件概率:前三类都没有触发时才出现
p_e = max(0.0, 1.0 - p_m - p_t - p_s)
return np.array([p_m, p_t, p_s, p_e], dtype=np.float64)
# ============================================================
# Step 3. 构造有限状态空间和状态转移矩阵
# ============================================================
def build_markov_chain(base_probs):
"""
构建有限状态机对应的马尔可夫链。
返回:
states: 所有状态列表
state_to_idx: 状态到矩阵下标的映射
trans_mat: 状态转移矩阵
room_probs: 每个状态下对应的四类房间概率
"""
p_m_base, p_t_base, p_s_base = base_probs
# 连续未出现次数的最大值
# 例如 p=0.1 时,最多连续 9 次没出;
# 到第 10 次时,该类型概率达到 100%
max_m = math.ceil(1.0 / p_m_base) - 1
max_t = math.ceil(1.0 / p_t_base) - 1
max_s = math.ceil(1.0 / p_s_base) - 1
# 枚举所有状态 (m, t, s)
states = list(itertools.product(
range(max_m + 1),
range(max_t + 1),
range(max_s + 1)
))
state_to_idx = {state: idx for idx, state in enumerate(states)}
num_states = len(states)
# trans_mat[i, j] 表示从状态 i 转移到状态 j 的概率
trans_mat = np.zeros((num_states, num_states), dtype=np.float64)
# room_probs[i] 表示状态 i 下 [怪物, 宝箱, 商店, 事件] 的概率
room_probs = np.zeros((num_states, 4), dtype=np.float64)
for i, state in enumerate(states):
m, t, s = state
# 当前状态下四类房间概率
probs = calc_current_probs(state, base_probs)
room_probs[i] = probs
# 出怪物:
# 怪物计数归零,宝箱和商店的连续未出现次数 +1
next_if_monster = (
0,
min(t + 1, max_t),
min(s + 1, max_s)
)
# 出宝箱:
# 宝箱计数归零,怪物和商店的连续未出现次数 +1
next_if_treasure = (
min(m + 1, max_m),
0,
min(s + 1, max_s)
)
# 出商店:
# 商店计数归零,怪物和宝箱的连续未出现次数 +1
next_if_shop = (
min(m + 1, max_m),
min(t + 1, max_t),
0
)
# 出事件:
# 怪物、宝箱、商店都没出,所以三者连续未出现次数都 +1
next_if_event = (
min(m + 1, max_m),
min(t + 1, max_t),
min(s + 1, max_s)
)
next_states = [
next_if_monster,
next_if_treasure,
next_if_shop,
next_if_event
]
# 写入状态转移矩阵
for p, next_state in zip(probs, next_states):
j = state_to_idx[next_state]
trans_mat[i, j] += p
return states, state_to_idx, trans_mat, room_probs
# ============================================================
# Step 4. 计算平稳分布
# ============================================================
def calc_stationary_distribution(trans_mat, tol=1e-15, max_iter=100000):
"""
用幂迭代法计算平稳分布 pi。
平稳分布满足:
pi = pi @ trans_mat
"""
num_states = trans_mat.shape[0]
# 初始时假设所有状态均匀分布
pi = np.ones(num_states, dtype=np.float64) / num_states
for _ in range(max_iter):
new_pi = pi @ trans_mat
# 如果分布变化已经足够小,认为收敛
if np.max(np.abs(new_pi - pi)) < tol:
return new_pi
pi = new_pi
return pi
# ============================================================
# Step 5. 计算从新一层开始,前 N 次问号房的平均期望
# ============================================================
def calc_first_n_average_probs(base_probs, max_n=10):
"""
计算从状态 (0, 0, 0) 开始,
进入前 1~max_n 次问号房时,
四类房间的平均出现概率。
"""
states, state_to_idx, trans_mat, room_probs = build_markov_chain(base_probs)
# 初始状态:新一层刚开始,三类特殊概率都处于基础值
dist = np.zeros(len(states), dtype=np.float64)
dist[state_to_idx[(0, 0, 0)]] = 1.0
# 累计前若干次问号房的概率
cumulative_probs = np.zeros(4, dtype=np.float64)
result = []
for n in range(1, max_n + 1):
# 第 n 次问号房的四类房间概率
current_room_probs = dist @ room_probs
# 累加到前 n 次总期望中
cumulative_probs += current_room_probs
# 前 n 次平均概率
average_probs = cumulative_probs / n
result.append((n, average_probs.copy()))
# 进入下一次问号房前,更新状态分布
dist = dist @ trans_mat
return result
# ============================================================
# Step 6. 蒙特卡洛模拟验证
# ============================================================
def simulate(base_probs, num_trials=1_000_000, seed=42):
"""
用随机模拟验证理论结果。
模拟逻辑严格按照:
怪物 -> 宝箱 -> 商店 -> 事件
的顺序轮盘赌进行。
"""
rng = np.random.default_rng(seed)
p_m_base, p_t_base, p_s_base = base_probs
max_m = math.ceil(1.0 / p_m_base) - 1
max_t = math.ceil(1.0 / p_t_base) - 1
max_s = math.ceil(1.0 / p_s_base) - 1
# 初始状态
state = [0, 0, 0]
# 统计 [怪物, 宝箱, 商店, 事件] 出现次数
counts = np.zeros(4, dtype=np.int64)
for _ in range(num_trials):
probs = calc_current_probs(tuple(state), base_probs)
r = rng.random()
if r < probs[0]:
# 出怪物
room_idx = 0
state = [
0,
min(state[1] + 1, max_t),
min(state[2] + 1, max_s)
]
elif r < probs[0] + probs[1]:
# 出宝箱
room_idx = 1
state = [
min(state[0] + 1, max_m),
0,
min(state[2] + 1, max_s)
]
elif r < probs[0] + probs[1] + probs[2]:
# 出商店
room_idx = 2
state = [
min(state[0] + 1, max_m),
min(state[1] + 1, max_t),
0
]
else:
# 出事件
room_idx = 3
state = [
min(state[0] + 1, max_m),
min(state[1] + 1, max_t),
min(state[2] + 1, max_s)
]
counts[room_idx] += 1
return counts / num_trials
# ============================================================
# Step 7. 主程序
# ============================================================
def main():
# 构建马尔可夫链
states, state_to_idx, trans_mat, room_probs = build_markov_chain(BASE_PROBS)
# 计算平稳分布
stationary = calc_stationary_distribution(trans_mat)
# 长期来看,每类房间的出现概率
long_run_probs = stationary @ room_probs
print("状态总数:", len(states))
print("\n长期平稳分布:")
for name, p in zip(ROOM_NAMES, long_run_probs):
print(f"{name}: {p:.6%}")
print("\n前 1~10 次问号房平均期望:")
print("| 问号房次数 | 怪物 | 宝箱 | 商店 | 事件 |")
print("|---:|---:|---:|---:|---:|")
first_n_result = calc_first_n_average_probs(BASE_PROBS, max_n=10)
for n, probs in first_n_result:
print(
f"| {n} | "
f"{probs[0]:.4%} | "
f"{probs[1]:.4%} | "
f"{probs[2]:.4%} | "
f"{probs[3]:.4%} |"
)
# 模拟 1,000,000 次进行验证
sim_probs = simulate(BASE_PROBS, num_trials=1_000_000, seed=42)
print("\n模拟结果:")
for name, p in zip(ROOM_NAMES, sim_probs):
print(f"{name}: {p:.6%}")
if __name__ == "__main__":
main()
# ============================================================
# 运行结果示例
# ============================================================
#
# 状态总数: 17000
#
# 长期平稳分布:
# 怪物: 27.320808%
# 宝箱: 11.704681%
# 商店: 14.441581%
# 事件: 46.532930%
#
# 前 1~10 次问号房平均期望:
# | 问号房次数 | 怪物 | 宝箱 | 商店 | 事件 |
# |---:|---:|---:|---:|---:|
# | 1 | 10.0000% | 2.0000% | 3.0000% | 85.0000% |
# | 2 | 14.5000% | 2.9800% | 4.4550% | 78.0650% |
# | 3 | 18.1000% | 3.9208% | 5.8227% | 72.1565% |
# | 4 | 20.6425% | 4.8053% | 7.0680% | 67.4842% |
# | 5 | 22.2382% | 5.6198% | 8.1661% | 63.9759% |
# | 6 | 23.1702% | 6.3544% | 9.1045% | 61.3710% |
# | 7 | 23.7394% | 7.0037% | 9.8804% | 59.3765% |
# | 8 | 24.1508% | 7.5666% | 10.5072% | 57.7755% |
# | 9 | 24.4916% | 8.0459% | 11.0037% | 56.4588% |
# | 10 | 24.7795% | 8.4479% | 11.3918% | 55.3807% |
#
# 模拟结果:
# 怪物: 27.313300%
# 宝箱: 11.687100%
# 商店: 14.454000%
# 事件: 46.545600%