This commit is contained in:
Pavel Flegr 2026-05-07 19:10:23 +02:00
commit ff5d6ab91d
7 changed files with 388 additions and 84 deletions

View file

@ -7,6 +7,7 @@ import (
"log"
"net/http"
"prsi/internal/engine"
"sort"
"sync"
"github.com/gorilla/websocket"
@ -49,7 +50,7 @@ type wsMessage struct {
PlayerID string `json:"player_id,omitempty"`
PlayerName string `json:"player_name,omitempty"`
GameID string `json:"game_id,omitempty"`
// CardIndices supports multi-card play; CardIdx is kept for single-card backwards compat.
// CardIndices carries selected card index; CardIdx is kept for backwards compat.
CardIndices []int `json:"card_indices,omitempty"`
CardIdx *int `json:"card_idx,omitempty"`
ChosenSuit engine.Suit `json:"chosen_suit,omitempty"`
@ -227,6 +228,49 @@ func (h *Hub) HandleGetScript(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"script": script})
}
type lobbyInfo struct {
GameID string `json:"game_id"`
PlayerCount int `json:"player_count"`
Players []string `json:"players"`
State engine.GameState `json:"state"`
HasRules bool `json:"has_rules"`
}
// HandleListLobbies returns waiting rooms that can be joined.
func (h *Hub) HandleListLobbies(w http.ResponseWriter, r *http.Request) {
h.mu.RLock()
defer h.mu.RUnlock()
rooms := make([]lobbyInfo, 0, len(h.games))
for id, g := range h.games {
g.Mu.RLock()
if g.State == engine.Waiting && len(g.Players) < 4 {
names := make([]string, 0, len(g.Players))
for _, p := range g.Players {
names = append(names, p.Name)
}
rooms = append(rooms, lobbyInfo{
GameID: id,
PlayerCount: len(g.Players),
Players: names,
State: g.State,
HasRules: len(g.ActiveRules) > 0,
})
}
g.Mu.RUnlock()
}
sort.Slice(rooms, func(i, j int) bool {
if rooms[i].PlayerCount != rooms[j].PlayerCount {
return rooms[i].PlayerCount > rooms[j].PlayerCount
}
return rooms[i].GameID < rooms[j].GameID
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{"lobbies": rooms})
}
// ──────────────────────────────────────────────────────────────────────────────
// Broadcast helpers
// ──────────────────────────────────────────────────────────────────────────────
@ -262,17 +306,22 @@ func (h *Hub) broadcastRules(gameID string) {
// gameView is what we send to each client — identical to Game but Hand only
// contains the *receiving* player's cards; others get a count.
type gameView struct {
ID string `json:"id"`
State engine.GameState `json:"state"`
CurrentPlayer int `json:"current_player_index"`
ActiveSuit engine.Suit `json:"active_suit"`
PenaltyCards int `json:"penalty_cards"`
SkipNext bool `json:"skip_next"`
DiscardPile []engine.Card `json:"discard_pile"`
Winner *engine.Player `json:"winner"`
PendingChoice *engine.PendingChoice `json:"pending_choice,omitempty"`
ActiveRules []engine.ActiveRule `json:"active_rules"`
Players []playerView `json:"players"`
ID string `json:"id"`
State engine.GameState `json:"state"`
CurrentPlayer int `json:"current_player_index"`
TurnNumber int `json:"turn_number"`
RoundNumber int `json:"round_number"`
LastActionType string `json:"last_action_type,omitempty"`
LastActorIndex int `json:"last_actor_index,omitempty"`
ActiveSuit engine.Suit `json:"active_suit"`
PenaltyCards int `json:"penalty_cards"`
SkipNext bool `json:"skip_next"`
DiscardPile []engine.Card `json:"discard_pile"`
Winner *engine.Player `json:"winner"`
PendingChoice *engine.PendingChoice `json:"pending_choice,omitempty"`
ChoiceQueue []engine.PendingChoice `json:"choice_queue,omitempty"`
ActiveRules []engine.ActiveRule `json:"active_rules"`
Players []playerView `json:"players"`
}
type playerView struct {
@ -306,17 +355,22 @@ func buildView(g *engine.Game, receiverID string) gameView {
}
return gameView{
ID: g.ID,
State: g.State,
CurrentPlayer: g.CurrentPlayer,
ActiveSuit: g.ActiveSuit,
PenaltyCards: g.PenaltyCards,
SkipNext: g.SkipNext,
DiscardPile: discardTop,
Winner: g.Winner,
PendingChoice: g.PendingChoice,
ActiveRules: g.ActiveRules,
Players: players,
ID: g.ID,
State: g.State,
CurrentPlayer: g.CurrentPlayer,
TurnNumber: g.TurnNumber,
RoundNumber: g.RoundNumber,
LastActionType: g.LastActionType,
LastActorIndex: g.LastActorIndex,
ActiveSuit: g.ActiveSuit,
PenaltyCards: g.PenaltyCards,
SkipNext: g.SkipNext,
DiscardPile: discardTop,
Winner: g.Winner,
PendingChoice: g.PendingChoice,
ChoiceQueue: g.ChoiceQueue,
ActiveRules: g.ActiveRules,
Players: players,
}
}