914 lines
20 KiB
Go
914 lines
20 KiB
Go
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)
|
||
|
||
// 统计各种点数的数量
|
||
var singles, pairs, triples, quads []int
|
||
for v, c := range counts {
|
||
switch c {
|
||
case 1:
|
||
singles = append(singles, v)
|
||
case 2:
|
||
pairs = append(pairs, v)
|
||
case 3:
|
||
triples = append(triples, v)
|
||
case 4:
|
||
quads = append(quads, v)
|
||
}
|
||
}
|
||
|
||
// 炸弹(三张或以上相同)
|
||
if len(counts) == 1 && n >= 3 {
|
||
return models.CardTypeBomb
|
||
}
|
||
|
||
// 四带二(4张相同+2单张)
|
||
if len(quads) == 1 && len(singles) == 2 && len(pairs) == 0 && len(triples) == 0 && n == 6 {
|
||
return models.CardTypeFourWithTwo
|
||
}
|
||
|
||
// 四带两对(4张相同+2对)
|
||
if len(quads) == 1 && len(pairs) == 2 && len(singles) == 0 && len(triples) == 0 && n == 8 {
|
||
return models.CardTypeFourWithTwoPairs
|
||
}
|
||
|
||
// 飞机(连续的三张)
|
||
if len(triples) >= 2 && cl.isConsecutive(triples) {
|
||
planeCount := len(triples)
|
||
// 纯飞机
|
||
if n == planeCount*3 {
|
||
return models.CardTypeAirplane
|
||
}
|
||
// 飞机带单张
|
||
if n == planeCount*3+planeCount && len(singles) == planeCount {
|
||
return models.CardTypeAirplaneSingle
|
||
}
|
||
// 飞机带对子
|
||
if n == planeCount*3+planeCount*2 && len(pairs) == planeCount {
|
||
return models.CardTypeAirplanePair
|
||
}
|
||
}
|
||
|
||
// 单张
|
||
if n == 1 {
|
||
return models.CardTypeSingle
|
||
}
|
||
|
||
// 对子
|
||
if n == 2 && len(counts) == 1 {
|
||
return models.CardTypePair
|
||
}
|
||
|
||
// 三带一(3张相同+1单张)
|
||
if n == 4 && len(triples) == 1 && len(singles) == 1 {
|
||
return models.CardTypeTripleOne
|
||
}
|
||
|
||
// 三带一对(3张相同+1对)
|
||
if n == 5 && len(triples) == 1 && len(pairs) == 1 {
|
||
return models.CardTypeTriplePair
|
||
}
|
||
|
||
// 顺子
|
||
if cl.isStraight(sorted) {
|
||
return models.CardTypeStraight
|
||
}
|
||
|
||
// 连对
|
||
if cl.isDoubleStraight(sorted, counts) {
|
||
return models.CardTypeDoubleStraight
|
||
}
|
||
|
||
return models.CardTypeInvalid
|
||
}
|
||
|
||
func (cl *CardLogic) isConsecutive(values []int) bool {
|
||
if len(values) < 2 {
|
||
return true
|
||
}
|
||
sort.Ints(values)
|
||
for i := 1; i < len(values); i++ {
|
||
if values[i] != values[i-1]+1 {
|
||
return false
|
||
}
|
||
if values[i] >= 15 { // 2和王不能参与
|
||
return false
|
||
}
|
||
}
|
||
return values[0] < 15
|
||
}
|
||
|
||
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 {
|
||
// 先比张数,张数多的赢;张数相同再比牌值
|
||
myCount := cl.getBombCount(cards)
|
||
lastCount := cl.getBombCount(lastPlay.Cards)
|
||
if myCount != lastCount {
|
||
return myCount > lastCount
|
||
}
|
||
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
|
||
}
|
||
|
||
if cardType == models.CardTypeAirplane || cardType == models.CardTypeAirplaneSingle || cardType == models.CardTypeAirplanePair {
|
||
return cl.compareAirplanes(cards, lastPlay.Cards)
|
||
}
|
||
|
||
return cl.getMainValue(cards) > cl.getMainValue(lastPlay.Cards)
|
||
}
|
||
|
||
func (cl *CardLogic) compareAirplanes(cards, lastCards []models.Card) bool {
|
||
myTriples := cl.getTripleValues(cards)
|
||
lastTriples := cl.getTripleValues(lastCards)
|
||
if len(myTriples) != len(lastTriples) {
|
||
return false
|
||
}
|
||
sort.Ints(myTriples)
|
||
sort.Ints(lastTriples)
|
||
return myTriples[len(myTriples)-1] > lastTriples[len(lastTriples)-1]
|
||
}
|
||
|
||
func (cl *CardLogic) getTripleValues(cards []models.Card) []int {
|
||
counts := cl.getValueCounts(cards)
|
||
var triples []int
|
||
for v, c := range counts {
|
||
if c >= 3 {
|
||
triples = append(triples, v)
|
||
}
|
||
}
|
||
return triples
|
||
}
|
||
|
||
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
|
||
return maxValue
|
||
}
|
||
|
||
func (cl *CardLogic) getBombCount(cards []models.Card) int {
|
||
counts := cl.getValueCounts(cards)
|
||
for _, c := range counts {
|
||
if c >= 3 {
|
||
return c
|
||
}
|
||
}
|
||
return 0
|
||
}
|
||
|
||
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)
|
||
}
|