初始化斗地主残局版项目
This commit is contained in:
15
app/Dockerfile
Normal file
15
app/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.21-alpine AS builder
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
WORKDIR /app/cmd
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /server .
|
||||
|
||||
FROM alpine:3.18
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
WORKDIR /app
|
||||
COPY --from=builder /server .
|
||||
EXPOSE 8080
|
||||
CMD ["./server"]
|
||||
39
app/cmd/main.go
Normal file
39
app/cmd/main.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"doudizhu-server/internal/game"
|
||||
"doudizhu-server/internal/handlers"
|
||||
"doudizhu-server/internal/ws"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gameMgr := game.NewGameManager()
|
||||
hub := ws.NewHub(gameMgr)
|
||||
go hub.Run()
|
||||
|
||||
h := handlers.NewHandler(gameMgr, hub)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/rooms", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.CreateRoom(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/rooms/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
h.JoinRoom(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/api/ws", h.WebSocket)
|
||||
|
||||
log.Println("App server starting on :8080")
|
||||
if err := http.ListenAndServe(":8080", mux); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
7
app/go.mod
Normal file
7
app/go.mod
Normal file
@@ -0,0 +1,7 @@
|
||||
module doudizhu-server
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gorilla/websocket v1.5.1
|
||||
|
||||
require golang.org/x/net v0.17.0 // indirect
|
||||
4
app/go.sum
Normal file
4
app/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
34
app/internal/game/errors.go
Normal file
34
app/internal/game/errors.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"doudizhu-server/internal/models"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
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")
|
||||
ErrPlayerNotFound = errors.New("player not found")
|
||||
ErrNotEnoughPlayers = errors.New("not enough players")
|
||||
ErrPlayerNotReady = errors.New("player not ready")
|
||||
ErrNotYourTurn = errors.New("not your turn")
|
||||
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")
|
||||
)
|
||||
|
||||
func generateID() string {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func cardKey(c models.Card) string {
|
||||
return fmt.Sprintf("%d_%d", c.Suit, c.Value)
|
||||
}
|
||||
563
app/internal/game/game.go
Normal file
563
app/internal/game/game.go
Normal file
@@ -0,0 +1,563 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
96
app/internal/handlers/handlers.go
Normal file
96
app/internal/handlers/handlers.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"doudizhu-server/internal/game"
|
||||
"doudizhu-server/internal/models"
|
||||
"doudizhu-server/internal/ws"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
GameMgr *game.GameManager
|
||||
Hub *ws.Hub
|
||||
}
|
||||
|
||||
func NewHandler(gameMgr *game.GameManager, hub *ws.Hub) *Handler {
|
||||
return &Handler{GameMgr: gameMgr, Hub: hub}
|
||||
}
|
||||
|
||||
func (h *Handler) CreateRoom(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.CreateRoomRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
if req.PlayerName == "" {
|
||||
h.jsonError(w, http.StatusBadRequest, "player name required")
|
||||
return
|
||||
}
|
||||
if req.MaxPlayers < 2 || req.MaxPlayers > 10 {
|
||||
req.MaxPlayers = 4
|
||||
}
|
||||
|
||||
room, player := h.GameMgr.CreateRoom(req.PlayerName, req.MaxPlayers)
|
||||
h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": player.ID})
|
||||
}
|
||||
|
||||
func (h *Handler) JoinRoom(w http.ResponseWriter, r *http.Request) {
|
||||
roomID := strings.ToLower(extractRoomID(r.URL.Path))
|
||||
if roomID == "" {
|
||||
h.jsonError(w, http.StatusBadRequest, "room id required")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.JoinRoomRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
if req.PlayerName == "" {
|
||||
h.jsonError(w, http.StatusBadRequest, "player name required")
|
||||
return
|
||||
}
|
||||
|
||||
room, player, err := h.GameMgr.JoinRoom(roomID, req.PlayerName)
|
||||
if err != nil {
|
||||
h.jsonError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
h.jsonSuccess(w, map[string]string{"roomId": room.ID, "playerId": player.ID})
|
||||
}
|
||||
|
||||
func (h *Handler) WebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
ws.ServeWs(h.Hub, w, r)
|
||||
}
|
||||
|
||||
func (h *Handler) jsonSuccess(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.ApiResponse{
|
||||
Status: 200,
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) jsonError(w http.ResponseWriter, status int, msg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(models.ApiResponse{
|
||||
Status: status,
|
||||
Code: 1,
|
||||
Message: msg,
|
||||
})
|
||||
}
|
||||
|
||||
func extractRoomID(path string) string {
|
||||
parts := strings.Split(strings.Trim(path, "/"), "/")
|
||||
for i, p := range parts {
|
||||
if p == "rooms" && i+1 < len(parts) && parts[i+1] != "join" {
|
||||
return parts[i+1]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
103
app/internal/models/models.go
Normal file
103
app/internal/models/models.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package models
|
||||
|
||||
type Suit int
|
||||
|
||||
const (
|
||||
SuitHeart Suit = iota
|
||||
SuitDiamond
|
||||
SuitClub
|
||||
SuitSpade
|
||||
SuitJoker
|
||||
SuitSuper
|
||||
)
|
||||
|
||||
type Card struct {
|
||||
Suit Suit `json:"suit"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
type CardType int
|
||||
|
||||
const (
|
||||
CardTypeInvalid CardType = iota
|
||||
CardTypeSingle
|
||||
CardTypePair
|
||||
CardTypeTriple
|
||||
CardTypeTripleOne
|
||||
CardTypeTriplePair
|
||||
CardTypeStraight
|
||||
CardTypeDoubleStraight
|
||||
CardTypeBomb
|
||||
CardTypeRocket
|
||||
)
|
||||
|
||||
type Player struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Cards []Card `json:"cards,omitempty"`
|
||||
CardCount int `json:"cardCount"`
|
||||
IsReady bool `json:"isReady"`
|
||||
IsOnline bool `json:"isOnline"`
|
||||
}
|
||||
|
||||
type RoomState int
|
||||
|
||||
const (
|
||||
RoomStateWaiting RoomState = iota
|
||||
RoomStatePlaying
|
||||
RoomStateFinished
|
||||
)
|
||||
|
||||
type PlayRecord struct {
|
||||
PlayerID string `json:"playerId"`
|
||||
Cards []Card `json:"cards"`
|
||||
CardType CardType `json:"cardType"`
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
ID string `json:"id"`
|
||||
Players []*Player `json:"players"`
|
||||
CurrentTurn int `json:"currentTurn"`
|
||||
State RoomState `json:"state"`
|
||||
LastPlay *PlayRecord `json:"lastPlay,omitempty"`
|
||||
LastWinner string `json:"lastWinner,omitempty"`
|
||||
RoundCount int `json:"roundCount"`
|
||||
MaxPlayers int `json:"maxPlayers"`
|
||||
}
|
||||
|
||||
type MessageType string
|
||||
|
||||
const (
|
||||
MsgTypeJoin MessageType = "join"
|
||||
MsgTypeLeave MessageType = "leave"
|
||||
MsgTypeReady MessageType = "ready"
|
||||
MsgTypePlay MessageType = "play"
|
||||
MsgTypePass MessageType = "pass"
|
||||
MsgTypeState MessageType = "state"
|
||||
MsgTypeError MessageType = "error"
|
||||
MsgTypeChat MessageType = "chat"
|
||||
MsgTypeGameOver MessageType = "gameOver"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Type MessageType `json:"type"`
|
||||
PlayerID string `json:"playerId,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type ApiResponse struct {
|
||||
Status int `json:"status"`
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type CreateRoomRequest struct {
|
||||
PlayerName string `json:"playerName"`
|
||||
MaxPlayers int `json:"maxPlayers"`
|
||||
}
|
||||
|
||||
type JoinRoomRequest struct {
|
||||
PlayerName string `json:"playerName"`
|
||||
}
|
||||
288
app/internal/ws/hub.go
Normal file
288
app/internal/ws/hub.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"doudizhu-server/internal/game"
|
||||
"doudizhu-server/internal/models"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID string
|
||||
RoomID string
|
||||
Conn *websocket.Conn
|
||||
Send chan []byte
|
||||
Hub *Hub
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
Clients map[string]*Client
|
||||
Register chan *Client
|
||||
Unregister chan *Client
|
||||
GameMgr *game.GameManager
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewHub(gameMgr *game.GameManager) *Hub {
|
||||
return &Hub{
|
||||
Clients: make(map[string]*Client),
|
||||
Register: make(chan *Client, 64),
|
||||
Unregister: make(chan *Client, 64),
|
||||
GameMgr: gameMgr,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) Run() {
|
||||
for {
|
||||
select {
|
||||
case c := <-h.Register:
|
||||
h.mu.Lock()
|
||||
h.Clients[c.ID] = c
|
||||
h.mu.Unlock()
|
||||
case c := <-h.Unregister:
|
||||
h.mu.Lock()
|
||||
if _, ok := h.Clients[c.ID]; ok {
|
||||
delete(h.Clients, c.ID)
|
||||
close(c.Send)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
if c.RoomID != "" {
|
||||
h.GameMgr.LeaveRoom(c.RoomID, c.ID)
|
||||
h.broadcastRoomState(c.RoomID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) ReadPump() {
|
||||
defer func() {
|
||||
c.Hub.Unregister <- c
|
||||
c.Conn.Close()
|
||||
}()
|
||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
c.Conn.SetPongHandler(func(string) error {
|
||||
c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
return nil
|
||||
})
|
||||
for {
|
||||
_, data, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
c.handleMessage(data)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) WritePump() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
c.Conn.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-c.Send:
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if !ok {
|
||||
c.Conn.WriteMessage(websocket.CloseMessage, nil)
|
||||
return
|
||||
}
|
||||
if err := c.Conn.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) handleMessage(data []byte) {
|
||||
var msg models.Message
|
||||
if err := json.Unmarshal(data, &msg); err != nil {
|
||||
return
|
||||
}
|
||||
msg.Timestamp = time.Now().Unix()
|
||||
|
||||
switch msg.Type {
|
||||
case models.MsgTypeReady:
|
||||
ready := true
|
||||
if m, ok := msg.Data.(map[string]interface{}); ok {
|
||||
if r, ok := m["ready"].(bool); ok {
|
||||
ready = r
|
||||
}
|
||||
}
|
||||
c.Hub.GameMgr.SetReady(c.RoomID, c.ID, ready)
|
||||
c.Hub.broadcastRoomState(c.RoomID)
|
||||
c.tryStartGame()
|
||||
|
||||
case models.MsgTypePlay:
|
||||
cards := c.parseCards(msg.Data)
|
||||
if len(cards) == 0 {
|
||||
c.sendError("invalid cards")
|
||||
return
|
||||
}
|
||||
if err := c.Hub.GameMgr.PlayCards(c.RoomID, c.ID, cards); err != nil {
|
||||
c.sendError(err.Error())
|
||||
return
|
||||
}
|
||||
room, _ := c.Hub.GameMgr.GetRoom(c.RoomID)
|
||||
if room != nil && room.State == models.RoomStateFinished {
|
||||
c.Hub.broadcastToRoom(c.RoomID, models.Message{
|
||||
Type: models.MsgTypeGameOver,
|
||||
Data: map[string]string{"winnerId": room.LastWinner},
|
||||
})
|
||||
} else {
|
||||
c.Hub.broadcastRoomState(c.RoomID)
|
||||
}
|
||||
|
||||
case models.MsgTypePass:
|
||||
if err := c.Hub.GameMgr.Pass(c.RoomID, c.ID); err != nil {
|
||||
c.sendError(err.Error())
|
||||
return
|
||||
}
|
||||
c.Hub.broadcastRoomState(c.RoomID)
|
||||
|
||||
case models.MsgTypeChat:
|
||||
c.Hub.broadcastToRoom(c.RoomID, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) tryStartGame() {
|
||||
room, err := c.Hub.GameMgr.GetRoom(c.RoomID)
|
||||
if err != nil || len(room.Players) < 2 {
|
||||
return
|
||||
}
|
||||
for _, p := range room.Players {
|
||||
if !p.IsReady {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := c.Hub.GameMgr.StartGame(c.RoomID); err == nil {
|
||||
c.Hub.broadcastRoomState(c.RoomID)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) parseCards(data interface{}) []models.Card {
|
||||
cards := make([]models.Card, 0)
|
||||
m, ok := data.(map[string]interface{})
|
||||
if !ok {
|
||||
return cards
|
||||
}
|
||||
arr, ok := m["cards"].([]interface{})
|
||||
if !ok {
|
||||
return cards
|
||||
}
|
||||
for _, item := range arr {
|
||||
if cm, ok := item.(map[string]interface{}); ok {
|
||||
card := models.Card{}
|
||||
if suit, ok := cm["suit"].(float64); ok {
|
||||
card.Suit = models.Suit(int(suit))
|
||||
}
|
||||
if value, ok := cm["value"].(float64); ok {
|
||||
card.Value = int(value)
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
func (c *Client) sendError(msg string) {
|
||||
data, _ := json.Marshal(models.Message{
|
||||
Type: models.MsgTypeError,
|
||||
Data: msg,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
select {
|
||||
case c.Send <- data:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) broadcastToRoom(roomID string, msg models.Message) {
|
||||
data, _ := json.Marshal(msg)
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
for _, c := range h.Clients {
|
||||
if c.RoomID == roomID {
|
||||
select {
|
||||
case c.Send <- data:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) broadcastRoomState(roomID string) {
|
||||
h.mu.RLock()
|
||||
clients := make([]*Client, 0)
|
||||
for _, c := range h.Clients {
|
||||
if c.RoomID == roomID {
|
||||
clients = append(clients, c)
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
for _, c := range clients {
|
||||
state := h.GameMgr.GetRoomState(roomID, c.ID)
|
||||
if state != nil {
|
||||
data, _ := json.Marshal(models.Message{
|
||||
Type: models.MsgTypeState,
|
||||
Data: state,
|
||||
Timestamp: time.Now().Unix(),
|
||||
})
|
||||
select {
|
||||
case c.Send <- data:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
|
||||
roomID := strings.ToLower(r.URL.Query().Get("roomId"))
|
||||
playerID := r.URL.Query().Get("playerId")
|
||||
if roomID == "" || playerID == "" {
|
||||
http.Error(w, "missing params", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("ws upgrade error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
client := &Client{
|
||||
ID: playerID,
|
||||
RoomID: roomID,
|
||||
Conn: conn,
|
||||
Send: make(chan []byte, 256),
|
||||
Hub: hub,
|
||||
}
|
||||
|
||||
hub.mu.Lock()
|
||||
hub.Clients[client.ID] = client
|
||||
hub.mu.Unlock()
|
||||
|
||||
go client.WritePump()
|
||||
go client.ReadPump()
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
hub.broadcastRoomState(roomID)
|
||||
}
|
||||
Reference in New Issue
Block a user