一、PRD算法简介
PRD(Pseudo Random Distribution)算法,即伪随机分布算法,最早起源于游戏 War3 ,用于调整暴击分布,公式如下:
P(N) = C * N ,N = 1,2,3,······,T
T = ceil(1/C),ceil 为向上取整,如 ceil(1.2) = 2
N表示当前攻击的次数,P(N)表示当前攻击的暴击率,C为概率增量以及初始概率,T表示N的最大值,即暴击率的循环周期;如果某一次攻击产生了暴击,则需要将 N 重置为 1;如果这次攻击没有产生暴击,则 N + 1。
在 DotA2 的官方 wiki 中,列出了各暴击率对应的 C 值,而 C 值的计算代码并没有给出,本文提供计算 C 值的二分法。
二、由 C 值计算暴击率 p
逻辑推演:
设当前召唤师暴击率为 p
P(N) = C * N, N = 1,2,3,...,T
T = ceil(1/C),最大连续不暴击次数
p = 1/E(P(N))
E(P(N)) 为暴击的平 A 次数期望,如 暴击率 50% 时,期望为两下平 A 必出暴击
// 由 C 值计算暴击率 p
func EstimateCriticalChanceByC(c float64) float64 {
// 当前平 A 暴击率,与平 A 次数相关
currentAdCriticalChance := 0.0
// 当前累计的暴击率
currentSumCriticalChance := 0.0
// 平 A 次数的期望
ePn := 0.0
// 第 t 下必暴击
t := math.Ceil(1 / c)
// 计算平 A 次数期望
for i := float64(1); i <= t; i++ {
currentAdCriticalChance = i * c * (1 - currentSumCriticalChance)
//log.Println("currentAdCriticalChance => ", currentAdCriticalChance)
currentSumCriticalChance += currentAdCriticalChance
//log.Println("currentSumCriticalChance => ", currentSumCriticalChance)
ePn += i * currentAdCriticalChance
//log.Println("ePn => ", ePn)
}
// 返回估算的暴击率
return 1 / ePn
}
三、暴力计算
// 暴力计算
// 在测试时可以看到该方法的弊端
// 当计算精度不够时,则没有办法取出最优的近似解
func CalculateCByTraverse(expectCriticalChance float64) (float64, float64) {
log.Printf("Expect Critical Chance => %.2f %%", expectCriticalChance*100)
c := 0.01
estimateCriticalChance := 0.0
for {
estimateCriticalChance = EstimateCriticalChanceByC(c)
if expectCriticalChance-estimateCriticalChance >= 0.000001 && expectCriticalChance-estimateCriticalChance < 0.000010 {
log.Println("Actually Critical Chance => ", estimateCriticalChance)
log.Println("C => ", c)
break
}
c += 0.000001
// 出现精度无法匹配的情况
if c >= 1.0 {
log.Println("Actually Critical Chance => [null]")
log.Println("C => [null]")
c = 0.0
break
}
}
return c, estimateCriticalChance
}
四、二分查找法
计算 C 值的基本思路为二分查找法,从 0~p 之间查找 C 值,使得 P(N) 的平均概率近似等于 p 。
// 二分查找法
func CalculateCByBinarySearch(expectCriticalChance float64) (float64, float64) {
left := 0.0
right := expectCriticalChance
mid := 0.0
last := 1.0
estimateCriticalChance := 0.0
log.Printf("Expect Critical Chance => %.2f %%", expectCriticalChance*100)
for {
// 用 mid 作为 C 不断逼近正确值
mid = (left + right) / 2.0
// 用 C 估算处理的当期暴击率 estimateCriticalChance
estimateCriticalChance = EstimateCriticalChanceByC(mid)
// 前后两次计算结果相同,说明到了逼近极限,不与 expectCriticalChance 比较是因为有误差,可能永远无法再逼近
// 一开始差值 X 是最大的,然后逐渐变小,最终趋于 0
if math.Abs(estimateCriticalChance-last) <= 0.0 {
log.Println("Actually Critical Chance => ", estimateCriticalChance)
break
}
// 如果估算概率大于预期概率,则往左边查找,否则往右边查找
if estimateCriticalChance > expectCriticalChance {
right = mid
} else {
left = mid
}
// 将本次估算的暴击率作为下次逼近的标准,若前后两次计算结果相同,left == right ,说明到了逼近极限
last = estimateCriticalChance
}
log.Println("C => ", mid)
return mid, estimateCriticalChance
}
五、输出结果保存为CSV文件
package utils
import (
"encoding/csv"
"log"
"os"
)
func SaveAsCsv(csvFileName string, header []string, content [][]string) {
// 创建 csv 文件
file, err := os.Create(csvFileName)
if err != nil {
log.Fatalln(err.Error())
}
defer file.Close()
// 写入UTF-8 BOM,防止中文乱码
file.WriteString("\xEF\xBB\xBF")
writer := csv.NewWriter(file)
defer writer.Flush()
// Write CSV header
writer.Write(header)
for i := 0; i < len(content); i++ {
writer.Write(content[i])
}
}
六、测试
package test
import (
"demo/utils"
"fmt"
"testing"
)
func TestCalculateCByTraverse(t *testing.T) {
var res [][]string
for i, j := float64(5), 0; i < 100; i, j = i+5, j+1 {
c, estimateCriticalChance := utils.CalculateCByTraverse(i / 100)
res = append(res, []string{fmt.Sprintf("%f", c), fmt.Sprintf("%f", i/100), fmt.Sprintf("%f", estimateCriticalChance)})
}
//fmt.Println(res)
utils.SaveAsCsv("TestCalculateCByTraverse.csv",[]string{"C", "Nominal Chance", "Approximate C"},res)
}
func TestCalculateCByBinarySearch(t *testing.T) {
var res [][]string
for i, j := float64(5), 0; i < 100; i, j = i+5, j+1 {
c, estimateCriticalChance := utils.CalculateCByBinarySearch(i / 100)
res = append(res, []string{fmt.Sprintf("%f", c), fmt.Sprintf("%f", i/100), fmt.Sprintf("%f", estimateCriticalChance)})
}
//fmt.Println(res)
utils.SaveAsCsv("TestCalculateCByBinarySearch.csv",[]string{"C", "Nominal Chance", "Approximate C"},res)
}
--- PASS: TestCalculateCByTraverse (0.15s)
--- PASS: TestCalculateCByBinarySearch (0.01s)
对比 Dota2 官方数据 :