PRD 算法 Golang 实现

本文深入探讨了PRD(伪随机分布)算法在游戏中的应用,特别是在暴击率计算中的作用。通过二分法和暴力计算两种方式,求解概率增量C,以使暴击率的平均期望值接近预设值。文章提供了详细的算法实现,并通过测试输出结果,展示了两种方法的效率差异。最后,结果被保存为CSV文件以供进一步分析。
摘要由CSDN通过智能技术生成

参考:概率补偿算法:从PRD算法的缺点谈起

一、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 官方数据 :

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余衫马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值