bindbox-game/internal/api/activity/matching_game_app.go
邹方成 45815bfb7d chore: 清理无用文件与优化代码结构
refactor(utils): 修复密码哈希比较逻辑错误
feat(user): 新增按状态筛选优惠券接口
docs: 添加虚拟发货与任务中心相关文档
fix(wechat): 修正Code2Session上下文传递问题
test: 补充订单折扣与积分转换测试用例
build: 更新配置文件与构建脚本
style: 清理多余的空行与注释
2025-12-18 17:35:55 +08:00

427 lines
12 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 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())
}
}