package app import ( "bindbox-game/internal/code" "bindbox-game/internal/pkg/core" "bindbox-game/internal/repository/mysql/dao" "context" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/binary" "fmt" "net/http" "time" ) // CardType 卡牌类型 type CardType string // CardTypeConfig 卡牌类型配置(从数据库加载) type CardTypeConfig struct { Code CardType Name string ImageURL string Quantity int32 } // MatchingGame 对对碰游戏结构 type MatchingGame struct { serverSeed []byte serverSeedHash string nonce int64 cardConfigs []CardTypeConfig cards []CardType hand []CardType deck []CardType totalPairs int64 round int64 roundHistory []MatchingRoundResult } type MatchingCard struct { ID int `json:"id"` Type CardType `json:"type"` Name string `json:"name,omitempty"` ImageURL string `json:"image_url,omitempty"` } type MatchingRoundResult struct { Round int64 `json:"round"` HandBefore []CardType `json:"hand_before"` Pairs []MatchingPair `json:"pairs"` PairsCount int64 `json:"pairs_count"` DrawnCards []CardType `json:"drawn_cards"` HandAfter []CardType `json:"hand_after"` CanContinue bool `json:"can_continue"` } type MatchingPair struct { CardType CardType `json:"card_type"` Count int64 `json:"count"` } // loadCardTypesFromDB 从数据库加载启用的卡牌类型配置 func loadCardTypesFromDB(ctx context.Context, readDB *dao.Query) ([]CardTypeConfig, error) { items, err := readDB.MatchingCardTypes.WithContext(ctx).Where(readDB.MatchingCardTypes.Status.Eq(1)).Order(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 } // NewMatchingGameWithConfig 使用数据库配置创建游戏 func NewMatchingGameWithConfig(configs []CardTypeConfig) *MatchingGame { g := &MatchingGame{ cardConfigs: configs, roundHistory: []MatchingRoundResult{}, } // 生成服务器种子 g.serverSeed = make([]byte, 32) rand.Read(g.serverSeed) hash := sha256.Sum256(g.serverSeed) g.serverSeedHash = fmt.Sprintf("%x", hash) // 根据配置生成卡牌 totalCards := 0 for _, cfg := range configs { totalCards += int(cfg.Quantity) } g.cards = make([]CardType, 0, totalCards) for _, cfg := range configs { for i := int32(0); i < cfg.Quantity; i++ { g.cards = append(g.cards, cfg.Code) } } // 安全洗牌 g.secureShuffle() // 分配手牌和牌堆(手牌9张) handSize := 9 if len(g.cards) < handSize { handSize = len(g.cards) } g.hand = g.cards[:handSize] g.deck = g.cards[handSize:] return g } // getCardConfig 获取指定卡牌类型的配置 func (g *MatchingGame) getCardConfig(cardType CardType) *CardTypeConfig { for i := range g.cardConfigs { if g.cardConfigs[i].Code == cardType { return &g.cardConfigs[i] } } return nil } // 保留原有函数用于向后兼容(使用默认配置) func NewMatchingGame() *MatchingGame { defaultConfigs := []CardTypeConfig{ {Code: "A", Name: "类型A", Quantity: 9}, {Code: "B", Name: "类型B", Quantity: 9}, {Code: "C", Name: "类型C", Quantity: 9}, {Code: "D", Name: "类型D", Quantity: 9}, {Code: "E", Name: "类型E", Quantity: 9}, {Code: "F", Name: "类型F", Quantity: 9}, {Code: "G", Name: "类型G", Quantity: 9}, {Code: "H", Name: "类型H", Quantity: 9}, {Code: "I", Name: "类型I", Quantity: 9}, {Code: "J", Name: "类型J", Quantity: 9}, {Code: "K", Name: "类型K", Quantity: 9}, } return NewMatchingGameWithConfig(defaultConfigs) } // secureShuffle 使用 HMAC-SHA256 的 Fisher-Yates 洗牌 func (g *MatchingGame) secureShuffle() { n := len(g.cards) for i := n - 1; i > 0; i-- { j := g.secureRandInt(i+1, fmt.Sprintf("shuffle:%d", i)) g.cards[i], g.cards[j] = g.cards[j], g.cards[i] } } // secureRandInt 使用 HMAC-SHA256 生成安全随机数 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)) } // PlayRound 执行一轮游戏 func (g *MatchingGame) PlayRound(isFirst bool) MatchingRoundResult { g.round++ handBefore := make([]CardType, len(g.hand)) copy(handBefore, g.hand) result := MatchingRoundResult{ Round: g.round, HandBefore: handBefore, } // 第一轮:如果有类型A的卡牌,额外抽牌 if isFirst { appleCount := 0 for _, c := range g.hand { if c == "A" { appleCount++ } } if appleCount > 0 && len(g.deck) >= appleCount { extra := g.deck[:appleCount] g.hand = append(g.hand, extra...) g.deck = g.deck[appleCount:] result.DrawnCards = append(result.DrawnCards, extra...) } } // 统计每种牌的数量 counter := make(map[CardType]int) for _, c := range g.hand { counter[c]++ } // 找出配对 pairsCount := int64(0) remaining := []CardType{} for cardType, count := range counter { pairs := count / 2 if pairs > 0 { pairsCount += int64(pairs) result.Pairs = append(result.Pairs, MatchingPair{CardType: cardType, Count: int64(pairs * 2)}) } // 剩余单张 if count%2 == 1 { remaining = append(remaining, cardType) } } result.PairsCount = pairsCount g.totalPairs += pairsCount // 抽取新牌 if pairsCount > 0 { drawCount := int(pairsCount) if drawCount > len(g.deck) { drawCount = len(g.deck) } if drawCount > 0 { newCards := g.deck[:drawCount] g.deck = g.deck[drawCount:] remaining = append(remaining, newCards...) result.DrawnCards = append(result.DrawnCards, newCards...) } } g.hand = remaining handAfter := make([]CardType, len(g.hand)) copy(handAfter, g.hand) result.HandAfter = handAfter result.CanContinue = pairsCount > 0 g.roundHistory = append(g.roundHistory, result) return result } // GetGameState 获取游戏状态 func (g *MatchingGame) GetGameState() map[string]any { return map[string]any{ "hand": g.hand, "hand_count": len(g.hand), "deck_count": len(g.deck), "total_pairs": g.totalPairs, "round": g.round, "server_seed_hash": g.serverSeedHash, } } // ========== API Handlers ========== type matchingGameStartRequest struct { IssueID int64 `json:"issue_id"` } type matchingGameStartResponse struct { GameID string `json:"game_id"` Hand []MatchingCard `json:"hand"` DeckCount int `json:"deck_count"` ServerSeedHash string `json:"server_seed_hash"` FirstRound MatchingRoundResult `json:"first_round"` } type matchingGamePlayRequest struct { GameID string `json:"game_id"` } type matchingGamePlayResponse struct { Round MatchingRoundResult `json:"round"` TotalPairs int64 `json:"total_pairs"` GameOver bool `json:"game_over"` FinalState map[string]any `json:"final_state,omitempty"` } // 游戏会话存储(生产环境应使用 Redis) var gameSessionsV2 = make(map[string]*MatchingGame) // StartMatchingGame 开始对对碰游戏 // @Summary 开始对对碰游戏 // @Description 创建新的对对碰游戏会话,返回初始手牌和第一轮结果 // @Tags APP端.活动 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param RequestBody body matchingGameStartRequest true "请求参数" // @Success 200 {object} matchingGameStartResponse // @Failure 400 {object} code.Failure // @Router /api/app/matching/start [post] func (h *handler) StartMatchingGame() core.HandlerFunc { return func(ctx core.Context) { userID := int64(ctx.SessionUserInfo().Id) // 从数据库加载卡牌类型配置 configs, err := loadCardTypesFromDB(ctx.RequestContext(), h.readDB) if err != nil || len(configs) == 0 { // 如果数据库没有配置,使用默认配置 configs = []CardTypeConfig{ {Code: "A", Name: "类型A", Quantity: 9}, {Code: "B", Name: "类型B", Quantity: 9}, {Code: "C", Name: "类型C", Quantity: 9}, {Code: "D", Name: "类型D", Quantity: 9}, {Code: "E", Name: "类型E", Quantity: 9}, {Code: "F", Name: "类型F", Quantity: 9}, {Code: "G", Name: "类型G", Quantity: 9}, {Code: "H", Name: "类型H", Quantity: 9}, {Code: "I", Name: "类型I", Quantity: 9}, {Code: "J", Name: "类型J", Quantity: 9}, {Code: "K", Name: "类型K", Quantity: 9}, } } // 创建新游戏 game := NewMatchingGameWithConfig(configs) // 生成游戏ID gameID := fmt.Sprintf("MG%d%d", userID, time.Now().UnixNano()) // 存储游戏会话 gameSessionsV2[gameID] = game // 执行第一轮 firstRound := game.PlayRound(true) // 构建手牌展示(包含卡牌名称和图片) cards := make([]MatchingCard, len(game.hand)) for i, c := range game.hand { mc := MatchingCard{ID: i, Type: c} if cfg := game.getCardConfig(c); cfg != nil { mc.Name = cfg.Name mc.ImageURL = cfg.ImageURL } cards[i] = mc } rsp := &matchingGameStartResponse{ GameID: gameID, Hand: cards, DeckCount: len(game.deck), ServerSeedHash: game.serverSeedHash, FirstRound: firstRound, } ctx.Payload(rsp) } } // PlayMatchingGame 执行一轮对对碰游戏 // @Summary 执行一轮对对碰游戏 // @Description 执行一轮配对,返回配对结果和新抽取的牌 // @Tags APP端.活动 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param RequestBody body matchingGamePlayRequest true "请求参数" // @Success 200 {object} matchingGamePlayResponse // @Failure 400 {object} code.Failure // @Router /api/app/matching/play [post] func (h *handler) PlayMatchingGame() core.HandlerFunc { return func(ctx core.Context) { req := new(matchingGamePlayRequest) if err := ctx.ShouldBindJSON(req); err != nil || req.GameID == "" { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game_id required")) return } // 获取游戏会话 game, ok := gameSessionsV2[req.GameID] if !ok { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game session not found")) return } // 执行一轮 round := game.PlayRound(false) rsp := &matchingGamePlayResponse{ Round: round, TotalPairs: game.totalPairs, GameOver: !round.CanContinue, } // 如果游戏结束,返回最终状态 if !round.CanContinue { rsp.FinalState = game.GetGameState() // 清理会话 delete(gameSessionsV2, req.GameID) } ctx.Payload(rsp) } } // GetMatchingGameState 获取对对碰游戏状态 // @Summary 获取对对碰游戏状态 // @Description 获取当前游戏的完整状态 // @Tags APP端.活动 // @Accept json // @Produce json // @Security LoginVerifyToken // @Param game_id query string true "游戏ID" // @Success 200 {object} map[string]any // @Failure 400 {object} code.Failure // @Router /api/app/matching/state [get] func (h *handler) GetMatchingGameState() core.HandlerFunc { return func(ctx core.Context) { gameID := ctx.RequestInputParams().Get("game_id") if gameID == "" { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game_id required")) return } game, ok := gameSessionsV2[gameID] if !ok { ctx.AbortWithError(core.Error(http.StatusBadRequest, code.ParamBindError, "game session not found")) return } ctx.Payload(game.GetGameState()) } }