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() }