大客及路途的业务逻辑

This commit is contained in:
liuxiaobo 2025-06-10 22:29:29 +08:00
parent 51ad92a30c
commit 31c9af105d
8 changed files with 175 additions and 210 deletions

View File

@ -121,6 +121,24 @@ func (r *BaseRoom[Seat]) RemovePlayer(player IPlayer) {
}
}
func (r *BaseRoom[Seat]) RangePlayer(proc func(IPlayer) bool) {
for _, seat := range r.Seats {
if !seat.Empty() && !proc(seat.Player()) {
return
}
}
}
func (r *BaseRoom[Seat]) FilterPlayer(proc func(IPlayer) bool) []IPlayer {
players := make([]IPlayer, 0)
for _, seat := range r.Seats {
if !seat.Empty() && proc(seat.Player()) {
players = append(players, seat.Player())
}
}
return players
}
func (r *BaseRoom[Seat]) DebugSendMsg(user IPlayer, msgId pb.MsgId, msg proto.Message) {
log.Debug(r.UserLog(user.Id(), "send msg:%v %v", msgId, msg.String()))
}

View File

@ -11,6 +11,7 @@ import (
var (
UserBindServiceRedis *redis.Client
UserRedis *redis.Client
)
func InitRedis() {
@ -23,4 +24,11 @@ func InitRedis() {
return
}
utils.AutoSetRedisPool(UserBindServiceRedis)
UserRedis, err = db.InitRedis(cfg.Password, cfg.Host, cfg.Port, constant.Redis1User)
if err != nil {
log.Fatal(err.Error())
return
}
utils.AutoSetRedisPool(UserRedis)
}

View File

@ -0,0 +1,25 @@
package model
import (
"context"
"encoding/json"
"game/common/proto/pb"
)
const (
trendKey = "ColorTrend"
)
// 获取历史路途数据
func GetTrend() [][]pb.ColorType {
v := UserRedis.Get(context.Background(), trendKey).Val()
trend := make([][]pb.ColorType, 0)
_ = json.Unmarshal([]byte(v), &trend)
return trend
}
// 保存路途数据
func SetTrend(trend [][]pb.ColorType) {
v, _ := json.Marshal(trend)
_ = UserRedis.Set(context.Background(), trendKey, string(v), 0).Err()
}

View File

@ -1,152 +0,0 @@
package model
//import (
// "errors"
// "github.com/fox/fox/log"
// "golang.org/x/crypto/bcrypt"
// "gorm.io/gorm"
// "time"
//)
//
//const (
// AccountNormal = 1 // 正常
// AccountFrozen = 2 // 冻结
// AccountBanned = 3 // 封禁
//)
//
//// 玩家账户表
//type UserAccount struct {
// gorm.Model
// Username string `gorm:"type:varchar(32);uniqueIndex;not null"` // 用户名
// Password string `gorm:"type:varchar(255);not null"` // 密码哈希
// Email string `gorm:"type:varchar(100)"` // 邮箱(可选)
// Phone string `gorm:"type:varchar(20)"` // 手机号(可选)
// DeviceID string `gorm:"type:varchar(64);index"` // 设备ID
// LastLoginIP string `gorm:"type:varchar(45)"` // 最后登录IP(支持IPv6)
// LastLoginTime time.Time // 最后登录时间
// Status int `gorm:"type:tinyint;default:1"` // 账号状态 1-正常 2-冻结 3-封禁
// RegisterIP string `gorm:"type:varchar(45)"` // 注册IP
// RegisterTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 注册时间
//}
//
//// 玩家登录记录表
//type UserLoginLog struct {
// gorm.Model
// PlayerID uint `gorm:"index"` // 关联玩家ID
// LoginIP string `gorm:"type:varchar(45);not null"` // 登录IP
// LoginTime time.Time `gorm:"type:TIMESTAMP;default:CURRENT_TIMESTAMP"` // 登录时间
// DeviceInfo string `gorm:"type:varchar(255)"` // 设备信息(JSON格式)
// LoginResult bool // 登录结果 true-成功 false-失败
// FailReason string `gorm:"type:varchar(100)"` // 失败原因
//}
//
//type UserLoginOp struct {
// db *gorm.DB
// logDb *gorm.DB
//}
//
//func NewUserLoginOp() *UserLoginOp {
// return &UserLoginOp{db: UserDB, logDb: LogDB}
//}
//
//var (
// ErrUserOrPassword = errors.New("user or password was error")
// ErrAccountFrozen = errors.New("account frozen")
// ErrAccountBanned = errors.New("account banned")
//)
//
//func (s *UserLoginOp) Login(username, password, ip, deviceID string) (*UserAccount, error) {
// var user UserAccount
// err := s.db.Where("username = ?", username).First(&user).Error
// if err != nil {
// return nil, err
// }
// // 验证密码
// if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
// s.recordLoginLog(user.ID, ip, deviceID, false, ErrUserOrPassword.Error())
// return nil, ErrUserOrPassword
// }
// // 检查账号状态
// switch user.Status {
// case AccountNormal:
//
// case AccountFrozen:
// s.recordLoginLog(user.ID, ip, deviceID, false, ErrAccountFrozen.Error())
// return nil, ErrAccountFrozen
// case AccountBanned:
// s.recordLoginLog(user.ID, ip, deviceID, false, ErrAccountBanned.Error())
// return nil, ErrAccountBanned
// }
// // 更新最后登录信息
// user.LastLoginIP = ip
// user.LastLoginTime = time.Now()
// _ = s.db.Save(&user).Error
//
// // 记录成功登录日志
// s.recordLoginLog(user.ID, ip, deviceID, true, "")
//
// // 6. 生成访问令牌
// token, err := generateToken(user.ID, user.Username)
// if err != nil {
// return nil, err
// }
// user.Password = token
// return &user, err
//}
//
//// 注册新用户
//func (s *UserLoginOp) RegisterNewUser(username, password, ip, deviceID string) (*UserAccount, error) {
// // 密码加密
// hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
// if err != nil {
// return nil, err
// }
// user := UserAccount{
// Username: username,
// Password: string(hashedPassword),
// DeviceID: deviceID,
// RegisterIP: ip,
// Status: 1,
// LastLoginIP: ip,
// LastLoginTime: time.Now(),
// }
//
// if err := s.db.Create(&user).Error; err != nil {
// return nil, err
// }
//
// s.recordLoginLog(user.ID, ip, deviceID, true, "")
//
// // 生成访问令牌
// token, err := generateToken(user.ID, user.Username)
// if err != nil {
// return nil, err
// }
// user.Password = token
//
// return &user, nil
//}
//
//// 记录登录日志
//func (s *UserLoginOp) recordLoginLog(userID uint, ip, deviceID string, success bool, failReason string) {
// logEntry := UserLoginLog{
// PlayerID: userID,
// LoginIP: ip,
// DeviceInfo: deviceID,
// LoginResult: success,
// FailReason: failReason,
// }
//
// if err := s.logDb.Create(&logEntry).Error; err != nil {
// log.ErrorF("记录登录日志失败: %v", err)
// }
//}
//
//// 生成JWT令牌(简化版)
//func generateToken(userID uint, username string) (string, error) {
// _ = userID
// _ = username
// // 这里应该使用JWT库生成实际令牌
// // 简化实现实际项目中请使用安全的JWT实现
// return "generated-token-placeholder", nil
//}

View File

@ -17,8 +17,7 @@ type ColorRoom struct {
roomCfg *game.ColorRoomConfig
timingCfg *game.ColorGameTiming
status pb.ColorGameStatus
statusTime int64 // 毫秒时间戳
users map[int64]*ColorPlayer // 所有玩家
statusTime int64 // 毫秒时间戳
endBetAreaMul []*pb.ColorBetAreaMul // 下注结束后,每个区域更新是否爆奖以及实际赔率
ntfOpenThreeDice *pb.NtfColorOpenThreeDice // 开骰子的消息
@ -26,40 +25,8 @@ type ColorRoom struct {
jackpotMgr *jackpot.JackPotMgr // jackpotValue
jackpotValue int64 // jackpot值
jackpotUser map[int64]int64 // 本局中jackpot的玩家用于游戏内广播
// ---------------------------------
// Status pb.ColorPinoyLiveGameStatus // 房间状态1 表示
// StatusTime int64 // 切换状态时的时间戳
//
// NormalDices []byte // 普通骰子 3个
// StartDices []byte // 初始位置摆放的骰子 3个
//
// // DefaultLuckyDice byte // 幸运骰子 1个 每局开始 放在拉杆上的骰子
// // DefaultNormalDices []byte // 普通骰子 3个 每局开始 放在拉杆上的骰子
//
// totalBets [BET_TYPE_NUM]int64 // 各区域的下注统计
// totalBet int64 // 下注统计
// //SceneInfo model.SceneInfo // 下注的玩家列表
//
// GameTrend *pb.ColorPinoyLiveTrend // 走势图
// //OnlineUserList []*model.User // 所有的玩家列表 用于排序
// PokerMsg *pb.ColorPinoyLivePokerMsg // 扑克消息
// RoomCfg game.ColorRoomConfig // 房间配置
// GameTiming game.ColorGameTiming // 时间配置
// startAt int64
// endAt int64
//
// ServerStatus int32
// wd *sync.WaitGroup
// TrendRedisKey string
// _aniLuckyDiceRouteIndex int32 // 幸运骰子动画路径
// _aniThreeDiceRouteIndex int32 // 3个骰子动画路径
// BigWinner []*pb.ColorPinoyLiveBigWinner
// LiveMgr *LiveMgr
// dealerName []string // 主播名字
// betEndBetAreasOdds []*pb.ColorPinoyLiveGameBetAreaInfo // 下注结束后,每个区域更新是否爆奖
// afterBetAreaOdds []*pb.ColorPinoyLiveBetAreaOdd // 开奖后,更新每个区域的赔率(主要是单色投注区开奖后三个赔率变为1个赔率)
bigUsers []*pb.ColorBigUser // 前6个大客户
trend [][]pb.ColorType // 路途
}
func newColorRoom(id, roomType int, srv service.IService) (baseroom.IRoom, pb.ErrCode) {
@ -75,6 +42,7 @@ func newColorRoom(id, roomType int, srv service.IService) (baseroom.IRoom, pb.Er
winBetAreaMul: nil,
jackpotMgr: jackpot.NewJackpotMgr(playType, model.UserRedis),
}
rm.trend = rm.initGameTrend()
code := pb.ErrCode_OK
rm.BaseRoom, code = baseroom.NewBaseRoom[ColorSeat](id, roomType, playType, srv)
@ -82,6 +50,7 @@ func newColorRoom(id, roomType int, srv service.IService) (baseroom.IRoom, pb.Er
log.ErrorF("new color room err code:%v", code)
return nil, code
}
// 初始化数据在OnInit之前完成
rm.OnInit()
return rm, code
}
@ -108,10 +77,13 @@ func (rm *ColorRoom) resetGameData() {
rm.initEndBetAreaMul()
rm.ntfOpenThreeDice = &pb.NtfColorOpenThreeDice{}
rm.jackpotValue = rm.jackpotMgr.GetJackpot()
rm.bigUsers = rm.bigUsers[0:0]
// 清理玩家身上数据
for _, user := range rm.users {
user.resetData()
for _, st := range rm.Seats {
if st.Player() != nil {
GetPlayer(st.Player()).resetData()
}
}
}

View File

@ -1,13 +1,16 @@
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"
"github.com/fox/fox/xtime"
"sort"
"sync"
)
@ -246,12 +249,13 @@ func (rm *ColorRoom) CalculateJackpotScore() (pb.ColorBetArea, map[int64]int64)
if winArea.PrizeType != pb.ColorPrizeType_CPT_Jackpot {
continue
}
for _, user := range rm.users {
if user.totalBets[winArea.Area] < 1 {
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])
}
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()))
@ -263,7 +267,8 @@ func (rm *ColorRoom) CalculateAllUserScore() {
// 赢钱会清空jackpot池
jpArea, userJackPot := rm.CalculateJackpotScore()
var jackpotUserName []string
for _, user := range rm.users {
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
if user.totalBet > 0 {
// 算分
jpScore := userJackPot[user.ID]
@ -273,7 +278,8 @@ func (rm *ColorRoom) CalculateAllUserScore() {
//rm.BroadHitJackpot(user, jpScore)
}
}
}
return true
})
}
// 计算 下注区域中奖得分
@ -334,8 +340,9 @@ func (rm *ColorRoom) settle() {
//var allWinner []*pb.ColorPinoyLiveBigWinner
rm.CalculateAllUserScore()
wg := new(sync.WaitGroup)
for _, user := range rm.users {
rm.RangePlayer(func(u baseroom.IPlayer) bool {
wg.Add(1)
user := GetPlayer(u)
ksync.GoSafe(func() {
defer wg.Done()
if user.totalBet > 0 {
@ -346,7 +353,8 @@ func (rm *ColorRoom) settle() {
//}
}
}, nil)
}
return true
})
wg.Wait()
//// 异步加钱完成后再执行后续的结算操作
//rm.Traverse(func(u *model.User) bool {
@ -370,6 +378,81 @@ func (rm *ColorRoom) settle() {
//rm.SetGameTrend()
}
// 更新大客户
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
}
//
// import (
// "encoding/json"

View File

@ -36,6 +36,8 @@ func (rm *ColorRoom) gameSettle() {
rm.setStatus(pb.ColorGameStatus_CGS_Settle)
rm.settle()
rm.notifySettle()
rm.updateGameTrend()
rm.notifyTrend()
rm.NewTimer(TtGameStart, time.Duration(rm.timingCfg.Settle)*time.Millisecond)
}

View File

@ -1,6 +1,9 @@
package room
import "game/common/proto/pb"
import (
"game/common/baseroom"
"game/common/proto/pb"
)
func (rm *ColorRoom) notifyGameStart() {
ntf := &pb.NtfColorGameStart{
@ -22,22 +25,28 @@ func (rm *ColorRoom) notifyEndBetting() {
Jackpot: rm.jackpotValue,
}
rm.Broadcast(pb.MsgId_NtfColorEndBettingId, ntf)
rm.notifyBigUsers()
}
func (rm *ColorRoom) notifyOpenThreeDice() {
ntf := &pb.NtfColorEndBetting{
// EndTime: rm.statusTime + rm.timingCfg.Start,
// Jackpot: 0,
}
rm.Broadcast(pb.MsgId_NtfColorOpenThreeDiceId, ntf)
rm.Broadcast(pb.MsgId_NtfColorOpenThreeDiceId, rm.ntfOpenThreeDice)
}
func (rm *ColorRoom) notifySettle() {
ntf := &pb.NtfColorEndBetting{
// EndTime: rm.statusTime + rm.timingCfg.Start,
// Jackpot: 0,
}
rm.Broadcast(pb.MsgId_NtfColorOpenThreeDiceId, ntf)
rm.RangePlayer(func(u baseroom.IPlayer) bool {
user := GetPlayer(u)
rm.SendMsg(user, pb.MsgId_NtfColorSettleId, user.settleMsg)
return true
})
rm.notifyBigUsers()
}
func (rm *ColorRoom) notifyBigUsers() {
rm.Broadcast(pb.MsgId_NtfColorBigUserId, &pb.NtfColorBigUser{BigUser: rm.bigUsers})
}
func (rm *ColorRoom) notifyTrend() {
rm.Broadcast(pb.MsgId_NtfColorTrendId, rm.getNotifyTrend())
}
//