564 lines
12 KiB
Go
564 lines
12 KiB
Go
package game
|
|
|
|
import (
|
|
"doudizhu-server/internal/models"
|
|
"math/rand"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
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 {
|
|
rooms map[string]*models.Room
|
|
players map[string]*models.Player
|
|
deck map[string][]models.Card
|
|
discard map[string][]models.Card
|
|
mu sync.RWMutex
|
|
cl *CardLogic
|
|
}
|
|
|
|
func NewGameManager() *GameManager {
|
|
return &GameManager{
|
|
rooms: make(map[string]*models.Room),
|
|
players: make(map[string]*models.Player),
|
|
deck: make(map[string][]models.Card),
|
|
discard: make(map[string][]models.Card),
|
|
cl: NewCardLogic(),
|
|
}
|
|
}
|
|
|
|
func (gm *GameManager) CreateRoom(playerName string, maxPlayers int) (*models.Room, *models.Player) {
|
|
gm.mu.Lock()
|
|
defer gm.mu.Unlock()
|
|
|
|
roomID := generateID()
|
|
playerID := generateID()
|
|
player := &models.Player{ID: playerID, 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.rooms[roomID] = room
|
|
gm.players[playerID] = player
|
|
gm.deck[roomID] = deck
|
|
gm.discard[roomID] = make([]models.Card, 0)
|
|
|
|
return room, player
|
|
}
|
|
|
|
func (gm *GameManager) JoinRoom(roomID, playerName string) (*models.Room, *models.Player, error) {
|
|
gm.mu.Lock()
|
|
defer gm.mu.Unlock()
|
|
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
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, Name: playerName, IsOnline: true}
|
|
room.Players = append(room.Players, player)
|
|
gm.players[playerID] = player
|
|
|
|
return room, player, nil
|
|
}
|
|
|
|
func (gm *GameManager) LeaveRoom(roomID, playerID string) error {
|
|
gm.mu.Lock()
|
|
defer gm.mu.Unlock()
|
|
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
return ErrRoomNotFound
|
|
}
|
|
|
|
for i, p := range room.Players {
|
|
if p.ID == playerID {
|
|
room.Players = append(room.Players[:i], room.Players[i+1:]...)
|
|
delete(gm.players, playerID)
|
|
if len(room.Players) == 0 {
|
|
delete(gm.rooms, roomID)
|
|
delete(gm.deck, roomID)
|
|
delete(gm.discard, roomID)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
return ErrPlayerNotFound
|
|
}
|
|
|
|
func (gm *GameManager) SetReady(roomID, playerID string, ready bool) error {
|
|
gm.mu.Lock()
|
|
defer gm.mu.Unlock()
|
|
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
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
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
return ErrPlayerNotFound
|
|
}
|
|
|
|
func (gm *GameManager) StartGame(roomID string) error {
|
|
gm.mu.Lock()
|
|
defer gm.mu.Unlock()
|
|
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
return ErrRoomNotFound
|
|
}
|
|
if len(room.Players) < 2 {
|
|
return ErrNotEnoughPlayers
|
|
}
|
|
if room.State == models.RoomStatePlaying {
|
|
return ErrGameStarted
|
|
}
|
|
|
|
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.deck[roomID] = deck
|
|
gm.discard[roomID] = make([]models.Card, 0)
|
|
|
|
room.State = models.RoomStatePlaying
|
|
room.RoundCount = 1
|
|
room.CurrentTurn = 0
|
|
room.LastPlay = nil
|
|
room.LastWinner = ""
|
|
|
|
gm.dealCards(roomID, 5)
|
|
return nil
|
|
}
|
|
|
|
func (gm *GameManager) dealCards(roomID string, count int) {
|
|
room := gm.rooms[roomID]
|
|
deck := gm.deck[roomID]
|
|
|
|
for i := 0; i < count; i++ {
|
|
for _, p := range room.Players {
|
|
if len(deck) == 0 {
|
|
deck = gm.discard[roomID]
|
|
gm.cl.Shuffle(deck)
|
|
gm.discard[roomID] = make([]models.Card, 0)
|
|
}
|
|
if len(deck) > 0 {
|
|
p.Cards = append(p.Cards, deck[0])
|
|
deck = deck[1:]
|
|
}
|
|
}
|
|
}
|
|
gm.deck[roomID] = 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()
|
|
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
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,
|
|
}
|
|
gm.discard[roomID] = append(gm.discard[roomID], cards...)
|
|
|
|
if len(player.Cards) == 0 {
|
|
room.LastWinner = playerID
|
|
room.State = models.RoomStateFinished
|
|
return nil
|
|
}
|
|
|
|
gm.nextTurn(room)
|
|
return nil
|
|
}
|
|
|
|
func (gm *GameManager) Pass(roomID, playerID string) error {
|
|
gm.mu.Lock()
|
|
defer gm.mu.Unlock()
|
|
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
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
|
|
}
|
|
|
|
gm.nextTurn(room)
|
|
|
|
if room.Players[room.CurrentTurn].ID == room.LastPlay.PlayerID {
|
|
room.LastPlay = nil
|
|
gm.dealCards(roomID, 1)
|
|
room.RoundCount++
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (gm *GameManager) nextTurn(room *models.Room) {
|
|
room.CurrentTurn = (room.CurrentTurn + 1) % len(room.Players)
|
|
}
|
|
|
|
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) GetRoom(roomID string) (*models.Room, error) {
|
|
gm.mu.RLock()
|
|
defer gm.mu.RUnlock()
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
return nil, ErrRoomNotFound
|
|
}
|
|
return room, nil
|
|
}
|
|
|
|
func (gm *GameManager) GetRoomState(roomID, playerID string) *models.Room {
|
|
gm.mu.RLock()
|
|
defer gm.mu.RUnlock()
|
|
|
|
room, ok := gm.rooms[roomID]
|
|
if !ok {
|
|
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,
|
|
}
|
|
}
|