P = {c: 1/13for c inrange(1, 10)}; P[10] = 4/13# 1 = A H17 = True
defadd(t, s, c): # (点数, 是否软) + 一张牌 t += c if c == 1and t + 10 <= 21: t += 10; s = True if t > 21and s: t -= 10; s = False return t, s
defdealer_dist(u): # 3.3: 庄家最终分布, 剔除 Blackjack dist = {17:0, 18:0, 19:0, 20:0, 21:0, 'bust':0} hole = {c: p for c, p in P.items() ifnot ((u==1and c==10) or (u==10and c==1))} norm = sum(hole.values()) defrec(t, s, pr): if t > 21: dist['bust'] += pr; return if t > 17or (t == 17andnot (s and H17)): dist[t] += pr; return for c, p in P.items(): rec(*add(t, s, c), pr*p) for c, p in hole.items(): rec(*add(*add(0, False, u), c), p/norm) return dist
D = {u: dealer_dist(u) for u inrange(1, 11)}
defev_stand(t, u): # 3.4: 停牌的期望 if t > 21: return -1.0 d = D[u] return d['bust'] + sum(d[f] for f inrange(17, t)) - sum(d[f] for f inrange(max(t+1,17), 22))
@lru_cache(None) defev_hit(t, s, u): # 3.6: 要牌的期望, 从 21 倒着算 returnsum(p * (-1if add(t,s,c)[0] > 21 elsemax(ev_stand(add(t,s,c)[0], u), ev_hit(*add(t,s,c), u))) for c, p in P.items())
defev_double(t, s, u): # 3.5: 双倍的期望 returnsum(p * (-2if add(t,s,c)[0] > 21else2*ev_stand(add(t,s,c)[0], u)) for c, p in P.items())
defbest2(t, s, u): # 起手两张: 停/要/双 取最大 o = {'S': ev_stand(t,u), 'H': ev_hit(t,s,u), 'D': ev_double(t,s,u)} returnmax(o, key=o.get), o
defev_split(c, u): # 3.7: 分牌 (A 只补一张, 允许 DAS) t0, s0 = add(0, False, c) e = sum(p * (ev_stand(add(t0,s0,c2)[0], u) if c == 1 elsemax(best2(*add(t0,s0,c2), u)[1].values())) for c2, p in P.items()) return2*e
if __name__ == '__main__': for t inrange(5, 21): # 硬牌表 print(t, [best2(t, False, u)[0] for u in [2,3,4,5,6,7,8,9,10,1]])
defadd(t, s, c): t += c if c == 1and t + 10 <= 21: t += 10; s = True if t > 21and s: t -= 10; s = False return t, s
defsolve(hand, u, N): deck = [4*N]*9 + [16*N] for c in hand + [u]: deck[c-1] -= 1 deck = tuple(deck) t, s = 0, False for c in hand: t, s = add(t, s, c) memoD, memoS, memoH = {}, {}, {}
defdrec(dk, dt, ds): # 庄家从 (dt,ds) 抽到停 if dt > 21: return (0,0,0,0,0,1) if dt > 17or (dt == 17andnot (ds and H17)): r = [0]*6; r[dt-17] = 1; returntuple(r) key = (dk, dt, ds) if key in memoD: return memoD[key] tot = sum(dk); acc = [0.0]*6 for i inrange(10): if dk[i]: nd = dk[:i] + (dk[i]-1,) + dk[i+1:] sub = drec(nd, *add(dt, ds, i+1)); w = dk[i]/tot for j inrange(6): acc[j] += w*sub[j] memoD[key] = tuple(acc); return memoD[key]
defdealer(dk): # 底牌分布, 剔除 Blackjack if dk in memoS: return memoS[dk] t0, s0 = add(0, False, u) acc = [0.0]*6 norm = sum(dk[i] for i inrange(10) ifnot ((u==1and i+1==10) or (u==10and i+1==1))) for i inrange(10): c = i+1 if dk[i]==0or (u==1and c==10) or (u==10and c==1): continue nd = dk[:i] + (dk[i]-1,) + dk[i+1:] sub = drec(nd, *add(t0, s0, c)); w = dk[i]/norm for j inrange(6): acc[j] += w*sub[j] memoS[dk] = acc; return acc
defev_stand(dk, pt): if pt > 21: return -1.0 d = dealer(dk); ev = d[5] for j, f inenumerate(range(17, 22)): if f < pt: ev += d[j] elif f > pt: ev -= d[j] return ev
defev_hit(dk, pt, ps): key = (dk, pt, ps) if key in memoH: return memoH[key] tot = sum(dk); ev = 0.0 for i inrange(10): if dk[i]: nd = dk[:i] + (dk[i]-1,) + dk[i+1:] nt, ns = add(pt, ps, i+1) ev += dk[i]/tot * (-1if nt > 21elsemax(ev_stand(nd, nt), ev_hit(nd, nt, ns))) memoH[key] = ev; return ev
defev_dbl(dk, pt, ps): tot = sum(dk); ev = 0.0 for i inrange(10): if dk[i]: nd = dk[:i] + (dk[i]-1,) + dk[i+1:] nt, _ = add(pt, ps, i+1) ev += dk[i]/tot * (-2if nt > 21else2*ev_stand(nd, nt)) return ev
defhouse_edge(bj_pay=1.5): # 6:5 传 1.2 ev = 0 for c1, p1 in P.items(): for c2, p2 in P.items(): for u, pu in P.items(): d = {1: 4/13, 10: 1/13}.get(u, 0) # 庄家 Blackjack 概率 if {c1, c2} == {1, 10}: v = (1-d)*bj_pay else: t, s = add(*add(0, False, c1), c2) e = max(best2(t, s, u)[1].values()) if c1 == c2: e = max(e, ev_split(c1, u)) v = -d + (1-d)*e ev += p1*p2*pu*v return -ev
memoH2 = {} defhit_dist(t, s, u): # 已拿过牌后按最优打, 最终点数分布 key = (t, s, u) if key in memoH2: return memoH2[key] if ev_stand(t, u) >= ev_hit(t, s, u): memoH2[key] = {t: 1.0}; return memoH2[key] out = defaultdict(float) for c, p in P.items(): nt, ns = add(t, s, c) if nt > 21: out['bust'] += p else: for k, q in hit_dist(nt, ns, u).items(): out[k] += p*q memoH2[key] = dict(out); return memoH2[key]
defhand_dist(t, s, u): # 起手两张按最优打, (最终点数, 乘数) 分布 a, _ = best2(t, s, u) if a == 'S': return {(t, 1): 1.0} out = defaultdict(float) for c, p in P.items(): nt, ns = add(t, s, c) if a == 'D': out[(nt if nt <= 21else'bust', 2)] += p elif nt > 21: out[('bust', 1)] += p else: for k, q in hit_dist(nt, ns, u).items(): out[(k, 1)] += p*q returndict(out)
defres(fin, m, f): # 对上庄家最终 f 的盈亏 if fin == 'bust': return -m if f == 'bust'or fin > f: return m return0if fin == f else -m
FIN = [17, 18, 19, 20, 21, 'bust'] defoutcome_nonsplit(t, s, u): hd = hand_dist(t, s, u); out = defaultdict(float) for f in FIN: for (fin, m), q in hd.items(): out[res(fin, m, f)] += D[u][f]*q return out
defoutcome_split(c, u): # 两手对同一个庄家结果, 给定 f 条件独立 side = defaultdict(float) t0, s0 = add(0, False, c) for c2, p in P.items(): t, s = add(t0, s0, c2) for k, q in ({(t, 1): 1.0} if c == 1else hand_dist(t, s, u)).items(): side[k] += p*q out = defaultdict(float) for f in FIN: cond = defaultdict(float) for (fin, m), q in side.items(): cond[res(fin, m, f)] += q for r1, q1 in cond.items(): for r2, q2 in cond.items(): out[r1+r2] += D[u][f]*q1*q2 return out
deftotal_dist(bj_pay=1.2): # 6.1: 1000 个开局按概率加权 tot = defaultdict(float) for c1, p1 in P.items(): for c2, p2 in P.items(): for u, pu in P.items(): w = p1*p2*pu d = {1: 4/13, 10: 1/13}.get(u, 0) if {c1, c2} == {1, 10}: tot[bj_pay] += w*(1-d); tot[0.0] += w*d; continue tot[-1.0] += w*d t, s = add(*add(0, False, c1), c2) base = max(best2(t, s, u)[1].values()) dist = outcome_split(c1, u) if (c1 == c2 and ev_split(c1, u) > base) else outcome_nonsplit(t, s, u) for k, q in dist.items(): tot[round(float(k), 4)] += w*(1-d)*q returndict(tot)
if __name__ == '__main__': td = total_dist(1.2) mu = sum(k*q for k, q in td.items()) sd = (sum(k*k*q for k, q in td.items()) - mu*mu)**0.5 for k insorted(td): print(f'{k:+.1f}: {td[k]*100:.2f}%') print('mean =', round(mu, 5), ' -house_edge(1.2) =', round(-house_edge(1.2), 5), ' sd =', round(sd, 4))
import random, bisect random.seed(3) vals = [-4, -3, -2, -1, 0, 1, 1.2, 2, 3, 4] # 6.1 的精确分布 prob = [.0002, .0018, .0446, .4337, .0878, .3231, .0451, .0610, .0023, .0004] cum = [] t = 0 for p in prob: t += p; cum.append(t) defdraw(): return vals[bisect.bisect(cum, random.random())]
defruin(bank, hands, trials=200000): # 6.2: n 手内中途输光的概率 b = 0 for _ inrange(trials): x = bank for _ inrange(hands): if x < 1: b += 1; break x += draw() return b / trials
if __name__ == '__main__': for bank in (3, 10, 20, 50): print(bank, '注:', [f'{ruin(bank, h):.0%}'for h in (30, 100, 300)])
A.5 收手时机
接在 A.4 之后运行, 对应 6.3 的表. 每行输出到达概率, 平均手数与整场期望:
1 2 3 4 5 6 7 8 9 10 11 12 13
defreach(bank, target, trials=50000): # 6.3: 输光之前先到 +target 的概率 hit = 0; steps = 0 for _ inrange(trials): x = bank; n = 0 while1 <= x < bank + target: x += draw(); n += 1 hit += x >= bank + target; steps += n return hit / trials, steps / trials
if __name__ == '__main__': for tgt in (5, 10, 20, 40): p, n = reach(20, tgt) print(f'目标 +{tgt}: 到达 {p:.0%}, 平均 {n:.0f} 手, 整场期望 {p*tgt-(1-p)*20:+.1f} 注')