401 lines
10 KiB
Go
Raw Permalink 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 activity
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"strings"
"sync"
"time"
)
// CardType 卡牌类型
type CardType string
// CardTypeConfig 卡牌类型配置
type CardTypeConfig struct {
Code CardType `json:"code"`
Name string `json:"name"`
ImageURL string `json:"image_url"`
Quantity int32 `json:"quantity"`
}
// MatchingCard 游戏中的卡牌实例
type MatchingCard struct {
ID string `json:"id"`
Type CardType `json:"type"`
}
// MatchingGame 对对碰游戏结构
type MatchingGame struct {
Mu sync.Mutex `json:"-"`
ServerSeed []byte `json:"server_seed"`
ServerSeedHash string `json:"server_seed_hash"`
Nonce int64 `json:"nonce"`
CardConfigs []CardTypeConfig `json:"card_configs"`
Deck []*MatchingCard `json:"deck"`
Board [9]*MatchingCard `json:"board"`
CardIDCounter int64 `json:"card_id_counter"`
TotalPairs int64 `json:"total_pairs"`
MaxPossiblePairs int64 `json:"max_possible_pairs"`
LastActivity time.Time `json:"last_activity"`
Position string `json:"position"` // 用户选择的类型,用于服务端验证
CreatedAt time.Time `json:"created_at"` // 游戏创建时间,用于自动开奖超时判断
ActivityID int64 `json:"activity_id"`
IssueID int64 `json:"issue_id"`
OrderID int64 `json:"order_id"`
UserID int64 `json:"user_id"`
}
const MatchingGameKeyPrefix = "bindbox:matching_game:"
// NewMatchingGameWithConfig 使用配置创建游戏
func NewMatchingGameWithConfig(configs []CardTypeConfig, position string, masterSeed []byte) *MatchingGame {
if len(masterSeed) == 0 {
return nil
}
g := &MatchingGame{
CardConfigs: configs,
Board: [9]*MatchingCard{},
LastActivity: time.Now(),
}
h := hmac.New(sha256.New, masterSeed)
h.Write([]byte(position))
h.Write([]byte(fmt.Sprintf("%d", time.Now().UnixNano())))
g.ServerSeed = h.Sum(nil)
hash := sha256.Sum256(g.ServerSeed)
g.ServerSeedHash = fmt.Sprintf("%x", hash)
totalCards := 0
for _, cfg := range configs {
totalCards += int(cfg.Quantity)
}
allCards := make([]*MatchingCard, 0, totalCards)
for _, cfg := range configs {
for i := int32(0); i < cfg.Quantity; i++ {
g.CardIDCounter++
id := fmt.Sprintf("c%d", g.CardIDCounter)
mc := &MatchingCard{
ID: id,
Type: cfg.Code,
}
allCards = append(allCards, mc)
}
}
g.Deck = allCards
g.SecureShuffle()
for i := 0; i < 9; i++ {
if len(g.Deck) > 0 {
card := g.Deck[0]
g.Deck = g.Deck[1:]
g.Board[i] = card
}
}
var theoreticalMax int64
for _, cfg := range configs {
theoreticalMax += int64(cfg.Quantity / 2)
}
g.MaxPossiblePairs = theoreticalMax
return g
}
func (g *MatchingGame) SecureShuffle() {
n := len(g.Deck)
for i := n - 1; i > 0; i-- {
j := g.secureRandInt(i+1, fmt.Sprintf("shuffle:%d", i))
g.Deck[i], g.Deck[j] = g.Deck[j], g.Deck[i]
}
}
func (g *MatchingGame) secureRandInt(max int, context string) int {
g.Nonce++
message := fmt.Sprintf("%s|nonce:%d", context, g.Nonce)
mac := hmac.New(sha256.New, g.ServerSeed)
mac.Write([]byte(message))
sum := mac.Sum(nil)
val := binary.BigEndian.Uint64(sum[:8])
return int(val % uint64(max))
}
// GetGameState 获取游戏状态
func (g *MatchingGame) GetGameState() map[string]any {
return map[string]any{
"board": g.Board,
"deck_count": len(g.Deck),
"total_pairs": g.TotalPairs,
"server_seed_hash": g.ServerSeedHash,
}
}
// SimulateMaxPairs 服务端模拟计算给定牌组和选中类型的理论最大对数
// 返回值:理论最大可消除对数
func (g *MatchingGame) SimulateMaxPairs() int64 {
// 重建完整牌组Board + Deck
allCards := make([]*MatchingCard, 0, len(g.Board)+len(g.Deck))
for _, c := range g.Board {
if c != nil {
allCards = append(allCards, c)
}
}
allCards = append(allCards, g.Deck...)
// 🔍 详细日志:牌组信息
boardTypes := make([]string, 0, 9)
for _, c := range g.Board {
if c != nil {
boardTypes = append(boardTypes, string(c.Type))
}
}
log.Printf("[SimulateMaxPairs] Board(%d张): %s", len(boardTypes), strings.Join(boardTypes, ","))
log.Printf("[SimulateMaxPairs] Deck长度: %d, 总牌数: %d", len(g.Deck), len(allCards))
log.Printf("[SimulateMaxPairs] Position: '%s'", g.Position)
if len(allCards) < 9 {
log.Printf("[SimulateMaxPairs] 牌数不足9张返回0")
return 0
}
selectedType := CardType(g.Position)
// 模拟游戏
hand := make([]*MatchingCard, 9)
copy(hand, allCards[:9])
deckIndex := 9
// 初始机会 = 手牌中选中类型的数量
chance := int64(0)
for _, c := range hand {
if c != nil && c.Type == selectedType {
chance++
}
}
// 🔍 详细日志:初始状态
handTypes := make([]string, 0, len(hand))
for _, c := range hand {
if c != nil {
handTypes = append(handTypes, string(c.Type))
}
}
log.Printf("[SimulateMaxPairs] 初始手牌: %s", strings.Join(handTypes, ","))
log.Printf("[SimulateMaxPairs] 选中类型: '%s', 初始机会: %d", selectedType, chance)
totalPairs := int64(0)
// canEliminate 检查是否有可配对的牌
canEliminate := func() CardType {
counts := make(map[CardType]int)
for _, c := range hand {
if c == nil {
continue
}
counts[c.Type]++
if counts[c.Type] >= 2 {
return c.Type
}
}
return ""
}
// eliminatePair 消除一对
eliminatePair := func(targetType CardType) bool {
first, second := -1, -1
for i, c := range hand {
if c == nil || c.Type != targetType {
continue
}
if first < 0 {
first = i
} else {
second = i
break
}
}
if first >= 0 && second >= 0 {
// 移除两张牌(从切片中删除)
newHand := make([]*MatchingCard, 0, len(hand)-2)
for i, c := range hand {
if i != first && i != second {
newHand = append(newHand, c)
}
}
hand = newHand
totalPairs++
chance++
return true
}
return false
}
// 游戏循环
guard := 0
for guard < 1000 {
guard++
// 尝试消除
if pairType := canEliminate(); pairType != "" {
eliminatePair(pairType)
continue
}
// 不能消除,尝试摸牌
if chance > 0 && deckIndex < len(allCards) {
newCard := allCards[deckIndex]
hand = append(hand, newCard)
deckIndex++
chance--
continue
}
// 既不能消除也不能摸牌,游戏结束
break
}
log.Printf("[SimulateMaxPairs] 模拟结束, 最终对数: %d, 剩余手牌: %d, 已摸牌数: %d", totalPairs, len(hand), deckIndex-9)
return totalPairs
}
// ReconstructMatchingGame 根据订单号还原游戏状态
func (s *service) ReconstructMatchingGame(ctx context.Context, orderNo string) (*MatchingGame, error) {
order, err := s.readDB.Orders.WithContext(ctx).Where(s.readDB.Orders.OrderNo.Eq(orderNo)).First()
if err != nil {
return nil, err
}
// 1. Get DrawLog first
drawLog, err := s.readDB.ActivityDrawLogs.WithContext(ctx).
Where(s.readDB.ActivityDrawLogs.OrderID.Eq(order.ID)).
First()
if err != nil {
return nil, fmt.Errorf("draw log not found: %w", err)
}
// 2. Get Receipt using DrawLogID
receipt, err := s.readDB.ActivityDrawReceipts.WithContext(ctx).
Where(s.readDB.ActivityDrawReceipts.DrawLogID.Eq(drawLog.ID)).
First()
if err != nil {
return nil, fmt.Errorf("draw receipt not found: %w", err)
}
serverSeed, err := hex.DecodeString(receipt.ServerSubSeed)
if err != nil || len(serverSeed) == 0 {
return nil, fmt.Errorf("invalid server seed in receipt")
}
// Retrieve ActivityID from Issue
issue, err := s.readDB.ActivityIssues.WithContext(ctx).Where(s.readDB.ActivityIssues.ID.Eq(drawLog.IssueID)).First()
var activityID int64
if err == nil && issue != nil {
activityID = issue.ActivityID
}
configs, err := s.loadCardTypesFromDB(ctx)
if err != nil || len(configs) == 0 {
return nil, fmt.Errorf("failed to load card types")
}
game := &MatchingGame{
CardConfigs: configs,
Board: [9]*MatchingCard{},
LastActivity: time.Now(),
ServerSeed: serverSeed,
ServerSeedHash: receipt.ServerSeedHash,
Nonce: 0,
IssueID: drawLog.IssueID,
OrderID: drawLog.OrderID,
UserID: order.UserID,
ActivityID: activityID,
}
allCards := make([]*MatchingCard, 0, 99)
for _, cfg := range configs {
for i := int32(0); i < cfg.Quantity; i++ {
game.CardIDCounter++
id := fmt.Sprintf("c%d", game.CardIDCounter)
mc := &MatchingCard{
ID: id,
Type: cfg.Code,
}
allCards = append(allCards, mc)
}
}
game.Deck = allCards
game.SecureShuffle()
for i := 0; i < 9; i++ {
if len(game.Deck) > 0 {
card := game.Deck[0]
game.Deck = game.Deck[1:]
game.Board[i] = card
}
}
var theoreticalMax int64
for _, cfg := range configs {
theoreticalMax += int64(cfg.Quantity / 2)
}
game.MaxPossiblePairs = theoreticalMax
return game, nil
}
func (s *service) ListMatchingCardTypes(ctx context.Context) ([]CardTypeConfig, error) {
return s.loadCardTypesFromDB(ctx)
}
func (s *service) loadCardTypesFromDB(ctx context.Context) ([]CardTypeConfig, error) {
items, err := s.readDB.MatchingCardTypes.WithContext(ctx).Where(s.readDB.MatchingCardTypes.Status.Eq(1)).Order(s.readDB.MatchingCardTypes.Sort.Asc()).Find()
if err != nil {
return nil, err
}
configs := make([]CardTypeConfig, len(items))
for i, item := range items {
configs[i] = CardTypeConfig{
Code: CardType(item.Code),
Name: item.Name,
ImageURL: item.ImageURL,
Quantity: item.Quantity,
}
}
return configs, nil
}
// GetMatchingGameFromRedis 从 Redis 加载游戏状态
func (s *service) GetMatchingGameFromRedis(ctx context.Context, gameID string) (*MatchingGame, error) {
data, err := s.redis.Get(ctx, MatchingGameKeyPrefix+gameID).Bytes()
if err != nil {
return nil, err
}
var game MatchingGame
if err := json.Unmarshal(data, &game); err != nil {
return nil, err
}
return &game, nil
}
// SaveMatchingGameToRedis 保存游戏状态到 Redis
func (s *service) SaveMatchingGameToRedis(ctx context.Context, gameID string, game *MatchingGame) error {
data, err := json.Marshal(game)
if err != nil {
return err
}
return s.redis.Set(ctx, MatchingGameKeyPrefix+gameID, data, 30*time.Minute).Err()
}