import SwiftUI struct GameBoardView: View { @ObservedObject var controller: GameController var body: some View { VStack(spacing: 0) { Group { if let snap = controller.snapshot { if controller.isRoundOver { roundOver(snap) } else { playingBoard(snap) } } else { waitingView } } .frame(maxWidth: .infinity, maxHeight: .infinity) shareBar .padding(.horizontal, 16) .padding(.vertical, 12) } } @ViewBuilder private var shareBar: some View { if controller.shareMode == .systemShare, let url = controller.joinURL { if controller.snapshot == nil { ShareLink(item: url) { Text("Share") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 12) } .buttonStyle(.borderedProminent) } else { Button { controller.tapNewGame() } label: { Text("Leave") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 12) } .buttonStyle(.borderedProminent) } } else { Button { controller.tapExit() } label: { Text("Exit") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 12) } .buttonStyle(.borderedProminent) } } private var waitingView: some View { VStack(spacing: 16) { if let id = controller.gameID { Text("Game #\(id)") .font(.title.weight(.bold)) } ProgressView() Text(controller.statusMessage ?? "Waiting for players…") .foregroundStyle(.secondary) if let error = controller.errorMessage { Text(error) .font(.footnote) .foregroundStyle(.red) .multilineTextAlignment(.center) } Button("New Game Instead") { controller.tapNewGame() } .buttonStyle(.bordered) } .padding(20) } private func playingBoard(_ snap: GolfAPISnapshot) -> some View { ScrollView { VStack(alignment: .leading, spacing: 16) { header(snap) if let error = controller.errorMessage { Text(error) .font(.footnote) .foregroundStyle(.red) } if controller.isEliminated { Text("You are eliminated.") .font(.headline) .foregroundStyle(.orange) } sourcesRow(snap) if let me = controller.myPlayer { VStack(alignment: .leading, spacing: 8) { Text(controller.isMyTurn ? "Your hand — tap a card, then deck or discard" : "Your hand (\(me.user))") .font(.headline) HandGridView( cards: handCards(for: me, cardNumber: snap.rules.cardNumber), interactive: controller.isMyTurn && !controller.isBusy, selectedSlot: controller.selectedSlot, compact: false, onSelect: { controller.selectSlot($0) } ) Text("Total \(me.points) · showing \(me.currentGamePoints)") .font(.caption) .foregroundStyle(.secondary) } } else { Text("You are not in this game’s player list yet.") .foregroundStyle(.secondary) } opponentsSection(snap) } .padding(16) } } private func header(_ snap: GolfAPISnapshot) -> some View { HStack { VStack(alignment: .leading, spacing: 2) { Text(controller.gameID.map { "Game #\($0)" } ?? "Golf") .font(.title2.weight(.bold)) Text(controller.isMyTurn ? "Your turn" : turnLabel(snap)) .font(.subheadline) .foregroundStyle(controller.isMyTurn ? Color.accentColor : .secondary) } Spacer() Text("Out at \(snap.rules.pointsToEnd)") .font(.caption.weight(.semibold)) .padding(.horizontal, 10) .padding(.vertical, 6) .background(Capsule().fill(Color.secondary.opacity(0.15))) } } private func turnLabel(_ snap: GolfAPISnapshot) -> String { if let current = snap.players.first(where: { $0.orderID == snap.currentPlayer }) { return "\(current.user)’s turn" } return "Waiting" } private func sourcesRow(_ snap: GolfAPISnapshot) -> some View { HStack(spacing: 20) { sourceButton( title: "Deck", subtitle: "\(snap.deckSize)", selected: controller.selectedSource == .deck, enabled: controller.isMyTurn && !controller.isBusy ) { controller.selectSource(.deck) } sourceButton( title: "Discard", subtitle: snap.discardTop.map { CardDisplay.label(for: UInt8(clamping: $0)) } ?? "—", selected: controller.selectedSource == .discard, enabled: controller.isMyTurn && !controller.isBusy ) { controller.selectSource(.discard) } Spacer() } } private func sourceButton( title: String, subtitle: String, selected: Bool, enabled: Bool, action: @escaping () -> Void ) -> some View { Button(action: action) { VStack(spacing: 6) { Text(title).font(.caption.weight(.semibold)) Text(subtitle).font(.system(.title3, design: .rounded).weight(.bold)) } .frame(width: 88, height: 72) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(selected ? Color.accentColor.opacity(0.2) : Color.secondary.opacity(0.12)) ) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .strokeBorder(selected ? Color.accentColor : .clear, lineWidth: 2) ) } .buttonStyle(.plain) .disabled(!enabled) .opacity(enabled ? 1 : 0.45) } private func opponentsSection(_ snap: GolfAPISnapshot) -> some View { VStack(alignment: .leading, spacing: 12) { Text("Table").font(.headline) ForEach(snap.players) { player in if player.user != controller.username { opponentRow(player: player, cardNumber: snap.rules.cardNumber) } } } } private func opponentRow(player: GolfAPISnapshot.Player, cardNumber: Int) -> some View { VStack(alignment: .leading, spacing: 6) { HStack { Text(player.lastMode == "eliminated" ? "\(player.user) (out)" : player.user) .font(.subheadline.weight(.semibold)) Spacer() Text("\(player.points) pts") .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } HandGridView( cards: handCards(for: player, cardNumber: cardNumber), interactive: false, selectedSlot: nil, compact: true, onSelect: nil ) } .padding(10) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color.secondary.opacity(0.08)) ) } private func roundOver(_ snap: GolfAPISnapshot) -> some View { ScrollView { VStack(alignment: .leading, spacing: 16) { Text("Round over") .font(.title.weight(.bold)) Text("All cards revealed. Continue when ready.") .font(.subheadline) .foregroundStyle(.secondary) ForEach(snap.players) { player in VStack(alignment: .leading, spacing: 6) { HStack { Text(player.user == controller.username ? "You (\(player.user))" : player.user) .font(.headline) if player.multiplier > 1 { Text("×\(player.multiplier)") .font(.caption.weight(.bold)) .padding(.horizontal, 6) .padding(.vertical, 2) .background(Capsule().fill(Color.orange.opacity(0.25))) } Spacer() Text("\(player.currentGamePoints) this hole · \(player.points) total") .font(.caption.monospacedDigit()) } HandGridView( cards: handCards(for: player, cardNumber: snap.rules.cardNumber), interactive: false, selectedSlot: nil, compact: true, onSelect: nil ) } } Button { controller.continueRound() } label: { Text(controller.isBusy ? "Updating…" : "Continue") .font(.headline) .frame(maxWidth: .infinity) .padding(.vertical, 12) } .buttonStyle(.borderedProminent) .disabled(controller.isBusy) } .padding(16) } } private func handCards(for player: GolfAPISnapshot.Player, cardNumber: Int) -> [HandCard] { var byPlacement: [Int: Int] = [:] for card in player.cards { byPlacement[card.cardPlacement] = card.card } return (1...cardNumber).map { placement in if let value = byPlacement[placement] { return HandCard(value: UInt8(clamping: value), faceUp: true) } return HandCard(value: 0, faceUp: false) } } }