2025-06-15 13:41:57 +08:00

524 lines
15 KiB
Go
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.

package room
import (
"game/common/baseroom"
"game/common/config/game"
"game/common/proto/pb"
"game/server/colorgame/config"
"game/server/colorgame/model"
"github.com/fox/fox/ksync"
"github.com/fox/fox/log"
"github.com/fox/fox/xrand"
"sort"
"sync"
"time"
)
const (
ReasonBet = "bet" // 扣钱原因:投注
ReasonSettle = "settle" // 扣钱或加钱原因:结算
)
func (rm *ColorRoom) setStatus(status pb.ColorGameStatus) {
rm.status = status
rm.statusTime = time.Now().UnixMilli()
}
// 状态结束时间点,毫秒
func (rm *ColorRoom) endTimeStatus() int64 {
duration := int64(0)
switch rm.status {
case pb.ColorGameStatus_CGS_Start:
duration = rm.timingCfg.Start
case pb.ColorGameStatus_CGS_Betting:
duration = rm.timingCfg.Betting
case pb.ColorGameStatus_CGS_BetEnd:
duration = rm.timingCfg.EndBetting
case pb.ColorGameStatus_CGS_OpenThreeDice:
duration = rm.timingCfg.OpenThreeDice
case pb.ColorGameStatus_CGS_Settle:
duration = rm.timingCfg.Settle
}
return rm.statusTime + duration
}
// 游戏开始前更新配置
func (rm *ColorRoom) updateConfig() {
colorConfig := config.GetColorConfig()
rm.timingCfg = colorConfig.GameTiming
for _, cfg := range colorConfig.Rooms {
if rm.RoomType() == cfg.RoomType {
rm.roomCfg = cfg
break
}
}
}
// 游戏开始时展示每个区域的默认赔率
func (rm *ColorRoom) initEndBetAreaMul() {
rm.endBetAreaMul = rm.endBetAreaMul[0:0]
for pos := 0; pos < len(pb.ColorBetArea_name); pos++ {
prizeArea := pb.ColorPrizeArea_CPA_Single_0
var mul = rm.roomCfg.WinSingleColorMul[0]
switch pos / 6 {
case 1:
prizeArea = pb.ColorPrizeArea_CPA_Double
mul = rm.roomCfg.WinDoubleColorMul
case 2:
prizeArea = pb.ColorPrizeArea_CPA_Three
mul = rm.roomCfg.WinThreeColorMul
}
rm.endBetAreaMul = append(rm.endBetAreaMul, &pb.ColorBetAreaMul{
Area: pb.ColorBetArea(pos),
PrizeArea: prizeArea,
PrizeType: pb.ColorPrizeType_CPT_Normal,
Mul: mul[0].Mul / config.Hundred,
})
}
}
// 随机出某个奖励档位下的赔率数组,档位,赔率数组概率之和
func (rm *ColorRoom) randArrMul(area pb.ColorBetArea) (arrMul []*game.ColorMulRate, prizeArea pb.ColorPrizeArea, sumRate int) {
switch area / 6 {
case 0:
maxWeight := 0
for _, w := range rm.roomCfg.WinSingleColorWeight {
maxWeight += w
}
weight := xrand.RandRange[int](0, maxWeight)
for pos, w := range rm.roomCfg.WinSingleColorWeight {
if weight < w {
prizeArea = pb.ColorPrizeArea(pos)
arrMul = rm.roomCfg.WinSingleColorMul[pos]
break
}
weight -= w
}
case 1:
prizeArea = pb.ColorPrizeArea_CPA_Double
arrMul = rm.roomCfg.WinDoubleColorMul
case 2:
prizeArea = pb.ColorPrizeArea_CPA_Three
arrMul = rm.roomCfg.WinThreeColorMul
default:
log.Error("area:%v is not exist")
return
}
for _, mr := range arrMul {
sumRate += mr.Rate
}
return
}
// 下注结束后,更新每个区域的实际赔率
func (rm *ColorRoom) updateEndBetAreaMul() {
jackpotExist := false
for pos := range pb.ColorBetArea_name {
bam := rm.endBetAreaMul[pos]
arrMul, prizeArea, sumRate := rm.randArrMul(pb.ColorBetArea(pos))
bam.PrizeArea = prizeArea
bam.PrizeType = pb.ColorPrizeType_CPT_Normal
if prizeArea == pb.ColorPrizeArea_CPA_Single_2 {
// 在单色区域 命中三同色爆奖之后 再随jackpot概率
// 只会有一个区域有jackpot标签
if !jackpotExist && xrand.RandRange(0, config.RateBase) < rm.roomCfg.JackpotRate {
bam.Mul = 0
bam.PrizeType = pb.ColorPrizeType_CPT_Jackpot
jackpotExist = true
continue
}
}
mulPos := 0
rd := xrand.RandRange[int](0, sumRate)
for i, mr := range arrMul {
if rd < mr.Rate {
mulPos = i
}
rd -= mr.Rate
}
// 赔率数组里第一个是基础赔率,后面的都是爆奖赔率
if mulPos > 0 {
bam.PrizeType = pb.ColorPrizeType_CPT_Big
}
bam.Mul = arrMul[mulPos].Mul / config.Hundred
log.Debug(rm.Log("区域:%v 显示赔率:%v 奖励类型:%v 奖励档位:%v", bam.Area, bam.Mul, bam.PrizeType, bam.PrizeArea))
}
}
// 获取三个骰子指定颜色中了几个
func (rm *ColorRoom) getDiceColorCount(color []pb.ColorType, c pb.ColorType) (count int) {
for _, colorType := range color {
if colorType == c {
count++
}
}
return
}
// 获取投注区域的颜色
func (rm *ColorRoom) getAreaColor(area pb.ColorBetArea) (color pb.ColorType) {
switch area {
case pb.ColorBetArea_CBA_Yellow, pb.ColorBetArea_CBA_Yellow2, pb.ColorBetArea_CBA_Yellow3:
return pb.ColorType_CT_Yellow
case pb.ColorBetArea_CBA_White, pb.ColorBetArea_CBA_White2, pb.ColorBetArea_CBA_White3:
return pb.ColorType_CT_White
case pb.ColorBetArea_CBA_Pink, pb.ColorBetArea_CBA_Pink2, pb.ColorBetArea_CBA_Pink3:
return pb.ColorType_CT_Pink
case pb.ColorBetArea_CBA_Blue, pb.ColorBetArea_CBA_Blue2, pb.ColorBetArea_CBA_Blue3:
return pb.ColorType_CT_Blue
case pb.ColorBetArea_CBA_Red, pb.ColorBetArea_CBA_Red2, pb.ColorBetArea_CBA_Red3:
return pb.ColorType_CT_Red
case pb.ColorBetArea_CBA_Green, pb.ColorBetArea_CBA_Green2, pb.ColorBetArea_CBA_Green3:
return pb.ColorType_CT_Green
}
return
}
// 获取某个区域的实际中奖赔率
func (rm *ColorRoom) getAreaMul(area pb.ColorBetArea, prizeArea pb.ColorPrizeArea) (mul int64, prizeType pb.ColorPrizeType) {
bam := rm.endBetAreaMul[area]
// 奖励档位一样比如单黄投注区域显示双色爆奖赔率为9。此时开出双黄色则该投注区域赔率为9。
// 如果开出三黄色则赔率为CPA_Single_2的赔率组里的第1个赔率(基础赔率)
// 如果开出单黄色则赔率为CPA_Single_0的赔率组里的第1个赔率(基础赔率)
if bam.PrizeArea == prizeArea {
mul = bam.Mul
prizeType = bam.PrizeType
} else {
arr2Mul := make([][]*game.ColorMulRate, 0, 5)
arr2Mul = append(arr2Mul, rm.roomCfg.WinSingleColorMul...)
arr2Mul = append(arr2Mul, rm.roomCfg.WinDoubleColorMul)
arr2Mul = append(arr2Mul, rm.roomCfg.WinThreeColorMul)
mul = arr2Mul[prizeArea][0].Mul
prizeType = pb.ColorPrizeType_CPT_Normal
}
return
}
// 检查投掷结果是否匹配某个下注区域
func (rm *ColorRoom) isWinInArea(color []pb.ColorType, area pb.ColorBetArea) (win bool, prizeArea pb.ColorPrizeArea) {
colorCount := rm.getDiceColorCount(color, rm.getAreaColor(area))
if colorCount == 0 {
return false, 0
}
// 单色投注区,查看开出几个该颜色
if area < pb.ColorBetArea_CBA_Yellow2 {
if colorCount == 1 {
return true, pb.ColorPrizeArea_CPA_Single_0
} else if colorCount == 2 {
return true, pb.ColorPrizeArea_CPA_Single_1
} else {
return true, pb.ColorPrizeArea_CPA_Single_2
}
} else if area < pb.ColorBetArea_CBA_Yellow3 {
if colorCount > 1 {
return true, pb.ColorPrizeArea_CPA_Double
}
} else {
if colorCount > 2 {
return true, pb.ColorPrizeArea_CPA_Three
}
}
return false, 0
}
// 开3个骰子并计算出赢钱区域及实际赔率
func (rm *ColorRoom) openDices() {
for i := 0; i < 3; i++ {
c := xrand.RandRange(int(pb.ColorType_CT_Yellow), int(pb.ColorType_CT_Green))
rm.ntfOpenThreeDice.Color = append(rm.ntfOpenThreeDice.Color, pb.ColorType(c))
}
rm.ntfOpenThreeDice.AniRouteIndex = 0
for _, area := range rm.endBetAreaMul {
isWin, prizeArea := rm.isWinInArea(rm.ntfOpenThreeDice.Color, area.Area)
mul, prizeType := rm.getAreaMul(area.Area, prizeArea)
if !isWin {
continue
}
rm.winBetAreaMul = append(rm.winBetAreaMul, &pb.ColorBetAreaMul{
Area: area.Area,
PrizeArea: prizeArea,
PrizeType: prizeType,
Mul: mul,
})
rm.ntfOpenThreeDice.WinArea = append(rm.ntfOpenThreeDice.WinArea, area.Area)
}
}
// 计算 下注区域中奖得分返回是否有jp奖jp奖位置中奖人数
func (rm *ColorRoom) calculateJackpotScore() (pb.ColorBetArea, map[int64]int64) {
jackpotArea := pb.ColorBetArea(-1)
for _, winArea := range rm.winBetAreaMul {
if winArea.PrizeType != pb.ColorPrizeType_CPT_Jackpot {
continue
}
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
if user.totalBets[winArea.Area] > 0 {
rm.jackpotMgr.AddJpUser(user.ID, user.totalBets[winArea.Area])
}
return true
})
}
rm.jackpotUser, rm.jackpotValue = rm.jackpotMgr.WinJackpot()
log.Debug(rm.Log("本局是否中jackpot奖:%v, 玩家一起分走jackpot:%v, 当前jackpot值:%v", rm.jackpotValue > 0, rm.jackpotValue, rm.jackpotMgr.GetJackpot()))
return jackpotArea, rm.jackpotUser
}
// 计算 所有玩家的下注区域中奖得分
func (rm *ColorRoom) calculateAllUserScore() {
// 赢钱会清空jackpot池
jpArea, userJackPot := rm.calculateJackpotScore()
var jackpotUserName []string
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
if user.totalBet > 0 {
// 算分
jpScore := userJackPot[user.ID]
rm.calculateUserScore(user, jpArea, jpScore)
if jpScore > 0 {
jackpotUserName = append(jackpotUserName, user.Nickname)
//rm.BroadHitJackpot(user, jpScore)
}
}
return true
})
}
// 计算 下注区域中奖得分
func (rm *ColorRoom) calculateUserScore(user *ColorPlayer, jpArea pb.ColorBetArea, jpScore int64) {
msg := user.settleMsg
for _, winArea := range rm.winBetAreaMul {
win2 := int64(0) // 税前
gold := int64(0) // 税后
if user.totalBets[winArea.Area] <= 0 {
continue
}
if jpArea != winArea.Area {
odds := winArea.Mul
// 奖金计算公式 Payouts =( Odds * Bet ) * ( 1 + Bonus) + Bet odds 是倍率 Bonus是猜中幸运骰子 的加成
// 本区域赢未扣税 倍率是百分值所以计算时要除以100
win2 = (odds * user.totalBets[winArea.Area]) / 100
// 本区域赢扣税
gold = win2 * (100 - rm.roomCfg.Rate) / 100
// 算税()
msg.Tax += win2 - gold
//// 加回投注本金
//gold += user.totalBets[winArea.Area]
msg.TotalWin += gold
// 统计赢区的下注总额
msg.TotalWinBaseBet += user.totalBets[winArea.Area]
log.Debug(rm.UserLog(user.ID, "算分 odds:%v 投注区:%v 税前:%v 税后+本金:%v", odds, winArea.Area, win2, gold))
} else {
win2 = jpScore
// 本区域赢扣税
gold = jpScore * (100 - rm.roomCfg.Rate) / 100
// 算税()
msg.Tax += win2 - gold
//// 加回投注本金
//gold += user.totalBets[winArea.Area]
msg.TotalWin += gold
// 统计赢区的下注总额
msg.TotalWinBaseBet += user.totalBets[winArea.Area]
log.Debug(rm.UserLog(user.ID, "算分 jackpotValue 投注区:%v win2:%v Gold:%v", winArea.Area, win2, gold))
}
msg.UserAreaWin = append(msg.UserAreaWin, &pb.NtfColorSettle_UserBetAreaMul{
AreaMul: &pb.ColorBetAreaMul{
Area: winArea.Area,
PrizeType: winArea.PrizeType,
PrizeArea: winArea.PrizeArea,
Mul: winArea.Mul,
},
Bet: user.totalBets[winArea.Area],
Win: win2,
RealWin: gold,
})
}
}
// 和平台做结算
func (rm *ColorRoom) settle() {
rm.calculateAllUserScore()
wg := new(sync.WaitGroup)
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
// 没有赢分
if user.settleMsg.TotalWin < 1 {
return true
}
ksync.GroupGo(wg, func() {
afterGold, ok := rm.rpcPayoutGold(user, user.settleMsg.TotalWin, ReasonSettle)
if !ok {
log.Error(rm.UserLog(user.ID, "add gold:%v fail", user.settleMsg.TotalWin))
} else {
rm.GetService().RunOnce(func() {
rm.setGold(user, afterGold)
})
}
})
return true
})
wg.Wait()
}
// 更新大客户
func (rm *ColorRoom) updateBigUsers() {
if rm.status > pb.ColorGameStatus_CGS_Betting && rm.status < pb.ColorGameStatus_CGS_Settle {
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
rm.bigUsers = append(rm.bigUsers, &pb.ColorBigUser{
NickName: user.Nickname,
Avatar: user.AvatarUrl,
WinChips: user.totalBet,
AreaId: user.totalBets,
})
return true
})
} else if rm.status >= pb.ColorGameStatus_CGS_Settle {
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
bigUser := &pb.ColorBigUser{
NickName: user.Nickname,
Avatar: user.AvatarUrl,
WinChips: user.settleMsg.TotalWin,
}
for _, area := range user.settleMsg.UserAreaWin {
bigUser.AreaId = append(bigUser.AreaId, area.Win)
}
return true
})
}
sort.Slice(rm.bigUsers, func(i, j int) bool {
return rm.bigUsers[i].WinChips > rm.bigUsers[j].WinChips
})
if len(rm.bigUsers) > 6 {
rm.bigUsers = rm.bigUsers[0:6]
}
}
func (rm *ColorRoom) initGameTrend() [][]pb.ColorType {
return model.GetTrend()
}
// 更新路途 最多保存100局
func (rm *ColorRoom) updateGameTrend() {
rm.trend = append(rm.trend, []pb.ColorType{rm.ntfOpenThreeDice.Color[0], rm.ntfOpenThreeDice.Color[1], rm.ntfOpenThreeDice.Color[2]})
if len(rm.trend) > 100 {
rm.trend = rm.trend[len(rm.trend)-100 : len(rm.trend)]
}
model.SetTrend(rm.trend)
}
// 更新路途 最多保存100局
func (rm *ColorRoom) getNotifyTrend() *pb.NtfColorTrend {
ntf := &pb.NtfColorTrend{}
// 只计算30局的概率
maxCount := 30
partTrend := rm.trend
if len(rm.trend) > maxCount {
partTrend = rm.trend[len(rm.trend)-maxCount : len(rm.trend)]
}
for c := pb.ColorType_CT_Yellow; c <= pb.ColorType_CT_Green; c++ {
cnt := int32(0)
for _, colors := range partTrend {
for _, color := range colors {
if color == c {
cnt++
}
}
}
ntf.ColorRate = append(ntf.ColorRate, &pb.NtfColorTrend_ColorRate{
Color: c,
Rate: cnt * 10000 / 100,
})
}
return ntf
}
// 五个档位(single有三个加上double,three两个)的赔率范围
func (rm *ColorRoom) initPrizeAreaRange() {
rm.prizeAreaRange = rm.prizeAreaRange[0:0]
arr2Mul := make([][]*game.ColorMulRate, 0, 5)
arr2Mul = append(arr2Mul, rm.roomCfg.WinSingleColorMul...)
arr2Mul = append(arr2Mul, rm.roomCfg.WinDoubleColorMul)
arr2Mul = append(arr2Mul, rm.roomCfg.WinThreeColorMul)
for prizeType, mul := range arr2Mul {
rm.prizeAreaRange = append(rm.prizeAreaRange, &pb.ColorPrizeAreaRange{
Pos: pb.ColorPrizeArea(prizeType),
MinMul: mul[0].Mul,
MaxMul: mul[len(mul)-1].Mul,
})
}
}
// 玩家房间配置信息,每个玩家下注档位不一样
func (rm *ColorRoom) getUserRoomConfig(user *ColorPlayer) *pb.ColorRoomConfig {
cnf := &pb.ColorRoomConfig{
BetList: nil,
MulRangeConfig: rm.prizeAreaRange,
}
lv := 0
for pos, lvBet := range rm.roomCfg.BetLevel {
if rm.GetGold(user) < lvBet {
break
}
lv = pos
}
cnf.BetList = rm.roomCfg.BetList[lv]
return cnf
}
// 玩家转换成proto格式
func (rm *ColorRoom) getProtoUser(user *ColorPlayer) *pb.ColorUser {
u := &pb.ColorUser{
UserID: user.ID,
Nick: user.Nickname,
Head: user.AvatarUrl,
Score: rm.GetGold(user),
}
return u
}
// 结束下注,扣除投注玩家的投注金额
func (rm *ColorRoom) endBetPayoutUser() {
var failer []*ColorPlayer
var mt sync.Mutex
wg := &sync.WaitGroup{}
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
// 没有下注
if user.totalBet < 1 {
return true
}
ksync.GroupGo(wg, func() {
afterGold, ok := rm.rpcPayoutGold(user, -user.totalBet, ReasonBet)
if !ok {
mt.Lock()
failer = append(failer, user)
mt.Unlock()
} else {
rm.GetService().RunOnce(func() {
rm.setGold(user, afterGold)
})
}
})
return true
})
wg.Wait()
// 移除扣钱失败玩家的投注信息
for _, u := range failer {
u.totalBet = 0
for pos := range u.totalBets {
u.totalBets[pos] = 0
}
rm.notifyPayoutFail(u, pb.ErrCode_GoldNotEnough)
}
}