package game import ( "context" "doudizhu-server/internal/models" "doudizhu-server/internal/redis" "encoding/json" "errors" "math/rand" "sort" "sync" "time" ) var ( ErrRoomNotFound = errors.New("room not found") ErrRoomFull = errors.New("room is full") ErrGameStarted = errors.New("game already started") ErrGameNotStarted = errors.New("game not started") ErrNotYourTurn = errors.New("not your turn") ErrPlayerNotFound = errors.New("player not found") ErrCardsNotInHand = errors.New("cards not in hand") ErrInvalidCardType = errors.New("invalid card type") ErrCannotBeat = errors.New("cannot beat last play") ErrCannotPass = errors.New("cannot pass") ErrNotEnoughPlayers = errors.New("not enough players") ) type CardLogic struct{} func NewCardLogic() *CardLogic { return &CardLogic{} } func (cl *CardLogic) CreateDeck() []models.Card { deck := make([]models.Card, 0, 55) suits := []models.Suit{models.SuitHeart, models.SuitDiamond, models.SuitClub, models.SuitSpade} for _, suit := range suits { for value := 3; value <= 15; value++ { deck = append(deck, models.Card{Suit: suit, Value: value}) } } deck = append(deck, models.Card{Suit: models.SuitJoker, Value: 16}) deck = append(deck, models.Card{Suit: models.SuitJoker, Value: 17}) deck = append(deck, models.Card{Suit: models.SuitSuper, Value: 18}) return deck } func (cl *CardLogic) Shuffle(deck []models.Card) { r := rand.New(rand.NewSource(time.Now().UnixNano())) r.Shuffle(len(deck), func(i, j int) { deck[i], deck[j] = deck[j], deck[i] }) } func (cl *CardLogic) SortCards(cards []models.Card) { sort.Slice(cards, func(i, j int) bool { if cards[i].Value != cards[j].Value { return cards[i].Value > cards[j].Value } return cards[i].Suit < cards[j].Suit }) } func (cl *CardLogic) GetCardType(cards []models.Card) models.CardType { n := len(cards) if n == 0 { return models.CardTypeInvalid } if cl.isRocket(cards) { return models.CardTypeRocket } sorted := make([]models.Card, n) copy(sorted, cards) cl.SortCards(sorted) counts := cl.getValueCounts(sorted) switch n { case 1: return models.CardTypeSingle case 2: if len(counts) == 1 { return models.CardTypePair } return models.CardTypeInvalid case 3: if len(counts) == 1 { return models.CardTypeBomb } if cl.isStraight(sorted) { return models.CardTypeStraight } return models.CardTypeInvalid case 4: if len(counts) == 1 { return models.CardTypeBomb } if len(counts) == 2 { for _, c := range counts { if c == 3 { return models.CardTypeTripleOne } } } if cl.isStraight(sorted) { return models.CardTypeStraight } if cl.isDoubleStraight(sorted, counts) { return models.CardTypeDoubleStraight } return models.CardTypeInvalid default: if cl.isStraight(sorted) { return models.CardTypeStraight } if cl.isDoubleStraight(sorted, counts) { return models.CardTypeDoubleStraight } hasTriple := false for _, c := range counts { if c >= 3 { hasTriple = true break } } if hasTriple && (n == 4 || n == 5) { return models.CardTypeTripleOne } return models.CardTypeInvalid } } func (cl *CardLogic) isRocket(cards []models.Card) bool { if len(cards) != 2 { return false } hasSmall, hasBig := false, false for _, c := range cards { if c.Suit == models.SuitJoker { if c.Value == 16 { hasSmall = true } else if c.Value == 17 { hasBig = true } } } return hasSmall && hasBig } func (cl *CardLogic) isStraight(cards []models.Card) bool { if len(cards) < 3 { return false } for _, c := range cards { if c.Value >= 15 || c.Suit == models.SuitJoker || c.Suit == models.SuitSuper { return false } } values := make([]int, len(cards)) for i, c := range cards { values[i] = c.Value } sort.Ints(values) for i := 1; i < len(values); i++ { if values[i] != values[i-1]+1 { return false } } return true } func (cl *CardLogic) isDoubleStraight(cards []models.Card, counts map[int]int) bool { if len(cards) < 4 || len(cards)%2 != 0 { return false } pairs := make([]int, 0) for v, c := range counts { if c != 2 { return false } if v >= 15 { return false } pairs = append(pairs, v) } sort.Ints(pairs) for i := 1; i < len(pairs); i++ { if pairs[i] != pairs[i-1]+1 { return false } } return true } func (cl *CardLogic) getValueCounts(cards []models.Card) map[int]int { counts := make(map[int]int) for _, c := range cards { counts[c.Value]++ } return counts } func (cl *CardLogic) CanPlay(cards []models.Card, lastPlay *models.PlayRecord) bool { cardType := cl.GetCardType(cards) if cardType == models.CardTypeInvalid { return false } if lastPlay == nil { return true } if cardType == models.CardTypeRocket { return true } if cardType == models.CardTypeBomb { if lastPlay.CardType == models.CardTypeBomb { return cl.getMainValue(cards) > cl.getMainValue(lastPlay.Cards) } if lastPlay.CardType == models.CardTypeRocket { return false } return true } if lastPlay.CardType == models.CardTypeBomb || lastPlay.CardType == models.CardTypeRocket { return false } if cardType != lastPlay.CardType || len(cards) != len(lastPlay.Cards) { return false } return cl.getMainValue(cards) > cl.getMainValue(lastPlay.Cards) } func (cl *CardLogic) getMainValue(cards []models.Card) int { counts := cl.getValueCounts(cards) maxValue, maxCount := 0, 0 for v, c := range counts { if c > maxCount || (c == maxCount && v > maxValue) { maxCount, maxValue = c, v } } return maxValue } type GameManager struct { rdb *redis.Client cl *CardLogic mu sync.RWMutex stopCleanup chan struct{} stopOnce sync.Once } func NewGameManager(rdb *redis.Client) *GameManager { gm := &GameManager{ rdb: rdb, cl: NewCardLogic(), stopCleanup: make(chan struct{}), } go gm.cleanupLoop() return gm } func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Room, *models.Player) { return gm.CreateRoomWithUser("", playerName, maxPlayers) } func (gm *GameManager) CreateRoomWithUser(userID, playerName string, maxPlayers int) (*models.Room, *models.Player) { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() roomID := generateID() playerID := generateID() player := &models.Player{ID: playerID, UserID: userID, Name: playerName, IsOnline: true} room := &models.Room{ ID: roomID, Players: []*models.Player{player}, MaxPlayers: maxPlayers, State: models.RoomStateWaiting, } deck := gm.cl.CreateDeck() gm.cl.Shuffle(deck) gm.saveRoom(ctx, room) gm.saveDeck(ctx, roomID, deck) gm.saveDiscard(ctx, roomID, []models.Card{}) return room, player } func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *models.Player, error) { return gm.JoinRoomWithUser(roomID, "", playerName) } func (gm *GameManager) JoinRoomWithUser(roomID, userID, playerName string) (*models.Room, *models.Player, error) { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return nil, nil, ErrRoomNotFound } if len(room.Players) >= room.MaxPlayers { return nil, nil, ErrRoomFull } if room.State == models.RoomStatePlaying { return nil, nil, ErrGameStarted } playerID := generateID() player := &models.Player{ID: playerID, UserID: userID, Name: playerName, IsOnline: true} room.Players = append(room.Players, player) gm.saveRoom(ctx, room) return room, player, nil } func (gm *GameManager) LeaveRoom(roomID, playerID string) error { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return ErrRoomNotFound } for i, p := range room.Players { if p.ID == playerID { room.Players = append(room.Players[:i], room.Players[i+1:]...) if len(room.Players) == 0 { gm.deleteRoomData(ctx, roomID) } else { gm.saveRoom(ctx, room) } return nil } } return ErrPlayerNotFound } func (gm *GameManager) LeaveRoomWithTTL(roomID, playerID string) error { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return ErrRoomNotFound } for _, p := range room.Players { if p.ID == playerID { p.IsOnline = false gm.saveRoom(ctx, room) gm.rdb.Set(ctx, redis.PlayerTTLKey(playerID), time.Now().Add(redis.PlayerTTL).Unix(), redis.PlayerTTL) return nil } } return ErrPlayerNotFound } func (gm *GameManager) MarkPlayerOnline(roomID, playerID string) { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return } for _, p := range room.Players { if p.ID == playerID { p.IsOnline = true gm.saveRoom(ctx, room) gm.rdb.Delete(ctx, redis.PlayerTTLKey(playerID)) return } } } func (gm *GameManager) MarkPlayerOffline(roomID, playerID string) { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return } for _, p := range room.Players { if p.ID == playerID { p.IsOnline = false gm.saveRoom(ctx, room) return } } } func (gm *GameManager) SetReady(roomID, playerID string, ready bool) error { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return ErrRoomNotFound } if room.State == models.RoomStatePlaying { return ErrGameStarted } for _, p := range room.Players { if p.ID == playerID { p.IsReady = ready if room.State == models.RoomStateFinished { room.State = models.RoomStateWaiting } gm.saveRoom(ctx, room) return nil } } return ErrPlayerNotFound } func (gm *GameManager) StartGame(roomID string) error { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return ErrRoomNotFound } if len(room.Players) < 2 { return ErrNotEnoughPlayers } if room.State == models.RoomStatePlaying { return ErrGameStarted } lastWinner := room.LastWinner for _, p := range room.Players { p.Cards = make([]models.Card, 0) p.CardCount = 0 p.IsReady = false } deck := gm.cl.CreateDeck() gm.cl.Shuffle(deck) gm.saveDeck(ctx, roomID, deck) gm.saveDiscard(ctx, roomID, []models.Card{}) room.State = models.RoomStatePlaying room.RoundCount = 1 room.LastPlay = nil if lastWinner != "" { for i, p := range room.Players { if p.ID == lastWinner { room.CurrentTurn = i break } } } else { room.CurrentTurn = 0 } gm.dealCards(room, 5) gm.saveRoom(ctx, room) return nil } func (gm *GameManager) dealCards(room *models.Room, count int) { ctx := context.Background() deck, _ := gm.loadDeck(ctx, room.ID) for i := 0; i < count; i++ { for _, p := range room.Players { if len(deck) == 0 { deck, _ = gm.loadDiscard(ctx, room.ID) gm.cl.Shuffle(deck) gm.saveDiscard(ctx, room.ID, []models.Card{}) } if len(deck) > 0 { p.Cards = append(p.Cards, deck[0]) deck = deck[1:] } } } gm.saveDeck(ctx, room.ID, deck) for _, p := range room.Players { gm.cl.SortCards(p.Cards) p.CardCount = len(p.Cards) } } func (gm *GameManager) PlayCards(roomID, playerID string, cards []models.Card) error { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return ErrRoomNotFound } if room.State != models.RoomStatePlaying { return ErrGameNotStarted } if room.Players[room.CurrentTurn].ID != playerID { return ErrNotYourTurn } player := room.Players[room.CurrentTurn] if !gm.hasCards(player, cards) { return ErrCardsNotInHand } cardType := gm.cl.GetCardType(cards) if cardType == models.CardTypeInvalid { return ErrInvalidCardType } if !gm.cl.CanPlay(cards, room.LastPlay) { return ErrCannotBeat } gm.removeCards(player, cards) player.CardCount = len(player.Cards) room.LastPlay = &models.PlayRecord{ PlayerID: playerID, Cards: cards, CardType: cardType, } discard, _ := gm.loadDiscard(ctx, roomID) discard = append(discard, cards...) gm.saveDiscard(ctx, roomID, discard) if len(player.Cards) == 0 { room.LastWinner = playerID room.State = models.RoomStateFinished gm.rdb.Set(ctx, redis.RoomTTLKey(roomID), time.Now().Add(redis.RoomTTL).Unix(), redis.RoomTTL) } else { room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players) } gm.saveRoom(ctx, room) return nil } func (gm *GameManager) Pass(roomID, playerID string) error { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() room, err := gm.loadRoom(ctx, roomID) if err != nil { return ErrRoomNotFound } if room.State != models.RoomStatePlaying { return ErrGameNotStarted } if room.Players[room.CurrentTurn].ID != playerID { return ErrNotYourTurn } if room.LastPlay == nil || room.LastPlay.PlayerID == playerID { return ErrCannotPass } room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players) // 一轮结束:回到最后出牌者,发牌并让他先出 if room.Players[room.CurrentTurn].ID == room.LastPlay.PlayerID { winnerID := room.LastPlay.PlayerID room.LastPlay = nil room.RoundCount++ gm.dealCards(room, 1) for i, p := range room.Players { if p.ID == winnerID { room.CurrentTurn = i break } } } gm.saveRoom(ctx, room) return nil } func (gm *GameManager) GetRoom(roomID string) (*models.Room, error) { gm.mu.RLock() defer gm.mu.RUnlock() return gm.loadRoom(context.Background(), roomID) } func (gm *GameManager) GetPlayerName(roomID, playerID string) string { gm.mu.RLock() defer gm.mu.RUnlock() room, err := gm.loadRoom(context.Background(), roomID) if err != nil { return "" } for _, p := range room.Players { if p.ID == playerID { return p.Name } } return "" } func (gm *GameManager) GetPlayerIDByUserID(roomID, userID string) string { gm.mu.RLock() defer gm.mu.RUnlock() room, err := gm.loadRoom(context.Background(), roomID) if err != nil { return "" } for _, p := range room.Players { if p.UserID == userID { return p.ID } } return "" } func (gm *GameManager) GetRoomState(roomID, playerID string) *models.Room { gm.mu.RLock() defer gm.mu.RUnlock() room, err := gm.loadRoom(context.Background(), roomID) if err != nil { return nil } players := make([]*models.Player, len(room.Players)) for i, p := range room.Players { pp := &models.Player{ ID: p.ID, Name: p.Name, IsReady: p.IsReady, IsOnline: p.IsOnline, CardCount: p.CardCount, } if p.ID == playerID { pp.Cards = p.Cards } players[i] = pp } return &models.Room{ ID: room.ID, Players: players, CurrentTurn: room.CurrentTurn, State: room.State, LastPlay: room.LastPlay, LastWinner: room.LastWinner, RoundCount: room.RoundCount, MaxPlayers: room.MaxPlayers, } } func (gm *GameManager) hasCards(player *models.Player, cards []models.Card) bool { hand := make(map[string]int) for _, c := range player.Cards { hand[cardKey(c)]++ } for _, c := range cards { k := cardKey(c) if hand[k] == 0 { return false } hand[k]-- } return true } func (gm *GameManager) removeCards(player *models.Player, cards []models.Card) { toRemove := make(map[string]int) for _, c := range cards { toRemove[cardKey(c)]++ } newCards := make([]models.Card, 0) removed := make(map[string]int) for _, c := range player.Cards { k := cardKey(c) if removed[k] < toRemove[k] { removed[k]++ } else { newCards = append(newCards, c) } } player.Cards = newCards } func (gm *GameManager) cleanupLoop() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for { select { case <-ticker.C: gm.cleanupExpired() case <-gm.stopCleanup: return } } } func (gm *GameManager) cleanupExpired() { gm.mu.Lock() defer gm.mu.Unlock() ctx := context.Background() gm.rdb.ScanKeys(ctx, "room:*:ttl", func(key string) error { var expiryTime int64 if err := gm.rdb.Get(ctx, key, &expiryTime); err != nil { return nil } if time.Now().Unix() > expiryTime { roomID := key[len("room:") : len(key)-len(":ttl")] gm.deleteRoomData(ctx, roomID) } return nil }) gm.rdb.ScanKeys(ctx, "player:*:ttl", func(key string) error { var expiryTime int64 if err := gm.rdb.Get(ctx, key, &expiryTime); err != nil { return nil } if time.Now().Unix() > expiryTime { playerID := key[len("player:") : len(key)-len(":ttl")] gm.rdb.Delete(ctx, redis.PlayerKey(playerID), redis.PlayerTTLKey(playerID)) } return nil }) } func (gm *GameManager) Stop() { gm.stopOnce.Do(func() { close(gm.stopCleanup) }) } func (gm *GameManager) saveRoom(ctx context.Context, room *models.Room) { gm.rdb.Set(ctx, redis.RoomKey(room.ID), room, redis.RoomTTL) } func (gm *GameManager) loadRoom(ctx context.Context, roomID string) (*models.Room, error) { var room models.Room if err := gm.rdb.Get(ctx, redis.RoomKey(roomID), &room); err != nil { return nil, err } return &room, nil } func (gm *GameManager) saveDeck(ctx context.Context, roomID string, deck []models.Card) { gm.rdb.Set(ctx, redis.RoomDeckKey(roomID), deck, redis.RoomTTL) } func (gm *GameManager) loadDeck(ctx context.Context, roomID string) ([]models.Card, error) { var deck []models.Card if err := gm.rdb.Get(ctx, redis.RoomDeckKey(roomID), &deck); err != nil { return nil, err } return deck, nil } func (gm *GameManager) saveDiscard(ctx context.Context, roomID string, discard []models.Card) { gm.rdb.Set(ctx, redis.RoomDiscardKey(roomID), discard, redis.RoomTTL) } func (gm *GameManager) loadDiscard(ctx context.Context, roomID string) ([]models.Card, error) { var discard []models.Card if err := gm.rdb.Get(ctx, redis.RoomDiscardKey(roomID), &discard); err != nil { return nil, err } return discard, nil } func (gm *GameManager) deleteRoomData(ctx context.Context, roomID string) { room, err := gm.loadRoom(ctx, roomID) if err == nil { for _, p := range room.Players { gm.rdb.Delete(ctx, redis.PlayerKey(p.ID), redis.PlayerTTLKey(p.ID)) } } gm.rdb.Delete(ctx, redis.RoomKey(roomID), redis.RoomDeckKey(roomID), redis.RoomDiscardKey(roomID), redis.RoomTTLKey(roomID), ) } func generateID() string { const charset = "abcdefghijklmnopqrstuvwxyz0123456789" b := make([]byte, 8) for i := range b { b[i] = charset[rand.Intn(len(charset))] } return string(b) } func cardKey(c models.Card) string { data, _ := json.Marshal(c) return string(data) }