import Foundation import WebSocketKit import CloudKit private let encoder = JSONEncoder() /** Specifies the number of cards of the called suit that a player must have to be allowed to play any card of the suit instead of having to play the ace. */ private let numberOfCardsToProtectAce = 4 let numberOfCardsPerPlayer = 8 final class Player { let name: PlayerName /// The player is the first to play a card in a new game var playsFirstCard = false /// The player is the next to perform an action (e.g. play a card) var isNextActor = false /// The player must select the game to play after winning the auction var selectsGame = false /// The players plays/played the first card for the current trick var startedCurrentTrick = false /// The action available to the player var actions: [Action] = [] /// Indicates if the player doubled ("legen") var didDoubleAfterFourCards: Bool? = nil /// Indicates if the player is still involved in the bidding process var isStillBidding = true /// Indicates that the player leads the game ("Spieler") var isGameLeader = false /// Indicates the number of raises ("Schuss") of the player var numberOfRaises = 0 /// The remaining cards of the player var handCards: [PlayableCard] = [] /// The card played for the current trick var playedCard: Card? = nil /// All tricks won by the player in this game var wonTricks: [Trick] = [] var socket: WebSocket? = nil var canOfferWedding: Bool { rawCards.canOfferWedding } var offersWedding = false var wouldAcceptWedding = false init(name: PlayerName) { self.name = name } var rawCards: [Card] { handCards.map { $0.card } } func has(card: Card) -> Bool { handCards.contains { $0.card == card } } func hasPlayable(card: Card) -> Bool { handCards.contains { $0.card == card && $0.isPlayable } } func remove(card: Card) { handCards = handCards.filter { $0.card != card } } func play(card: Card) { remove(card: card) playedCard = card actions = actions.filter { $0 != .doubleDuringGame } } func connect(using socket: WebSocket) { _ = self.socket?.close() self.socket = socket } func send(_ info: TableInfo) { try? socket?.send(encodeJSON(info)) } func disconnect() -> Bool { guard let socket = socket else { return false } do { try socket.close().wait() } catch { print("Failed to close socket for player: \(name): \(error)") } self.socket = nil return true } func canPerform(_ action: Action) -> Bool { actions.contains(action) } func prepareForNewGame(isFirstPlayer: Bool) { playsFirstCard = isFirstPlayer isNextActor = isFirstPlayer selectsGame = false startedCurrentTrick = isFirstPlayer actions = [.deal] didDoubleAfterFourCards = nil isStillBidding = true isGameLeader = false handCards = [] playedCard = nil wonTricks = [] } func assignFirstCards(_ cards: Hand) { actions = [.initialDoubleCost, .noDoubleCost] handCards = cards.map { .init(card: $0, isPlayable: false) } } func didDouble(_ double: Bool) { actions = [] didDoubleAfterFourCards = double } func assignRemainingCards(_ cards: Hand) { handCards = (rawCards + cards) .sortedCards(order: NormalCardOrder.self) .map { .init(card: $0, isPlayable: false) } } func startAuction() { if playsFirstCard { actions = [.withdrawFromAuction, .increaseOrMatchGame] } else { actions = [] } if canOfferWedding { actions.append(.offerWedding) } } func offerWedding() { offersWedding = true isStillBidding = false actions = [] } func weddingOfferExists() { guard isStillBidding else { return } actions = [.increaseOrMatchGame, .withdrawFromAuction] } func hasWeddingOffer() { guard isStillBidding else { return } actions = [.acceptWedding, .increaseOrMatchGame, .withdrawFromAuction] } func weddingOutbid() { isNextActor = false actions = [] offersWedding = false } func didPerformBid() { isNextActor = false actions = [] } func requiresBid() { isNextActor = true actions = [.increaseOrMatchGame, .withdrawFromAuction] } func acceptWedding() { wouldAcceptWedding = true actions = [] } func weddingAccepted() { guard isStillBidding else { actions = [] return } actions = [.increaseOrMatchGame, .withdrawFromAuction] } func auctionEnded() { actions = [] isStillBidding = false isNextActor = false } func mustSelectWeddingCard() { isNextActor = true // Only cards which are not trump can be given to the other player handCards = handCards.map { let card = $0.card return .init(card: card, isPlayable: !card.isTrump(in: .hochzeit)) } // Hochzeit costs double numberOfRaises += 1 } func canPlay(game: GameType) -> Bool { guard let suit = game.calledSuit else { if game == .hochzeit { return canOfferWedding } return true } let sorter = game.sortingType let cards = rawCards guard sorter.hasCardToCall(suit, in: cards) else { // Player needs at least one card of the called suit return false } let ace = Card(suit, .ass) return !cards.contains(ace) } func mustSelectGame() { isNextActor = true selectsGame = true } func replace(card: Card, with other: Card) { remove(card: card) handCards.append(.init(card: other, isPlayable: false)) } func replaceWeddingCard(with card: Card) -> Card { let index = handCards.firstIndex { $0.card.isTrump(in: .hochzeit) }! let removed = handCards.remove(at: index).card handCards.append(.init(card: card, isPlayable: false)) return removed } func start(game: GameType) { isNextActor = playsFirstCard startedCurrentTrick = playsFirstCard selectsGame = false actions = [.doubleDuringGame] isGameLeader = false if playsFirstCard { setPlayableCardsForStarter(game: game) } } func switchLeadership() { isGameLeader.toggle() if isGameLeader { actions = actions.filter { $0 != .doubleDuringGame } } else if !actions.contains(.doubleDuringGame) { actions.append(.doubleDuringGame) } } func withdrawFromBidding() { isNextActor = false isStillBidding = false actions = [] } func didFinishTrick(canDoubleInNextRound: Bool) { isNextActor = false playedCard = nil if canDoubleInNextRound, !isGameLeader { actions = [.doubleDuringGame] } else { actions = [] } } func didWin(trick: Trick) { self.wonTricks.append(trick) self.isNextActor = true } func setPlayableCards(forCurrentTrick trick: Trick, in game: GameType?) { guard let game = game, isNextActor else { for i in 0.. 1 else { // Last card can always be played setAllCards(playable: true) return } guard let first = trick.first else { setPlayableCardsForStarter(game: game) return } let sorter = game.sortingType guard sorter.isTrump(first) else { setPlayableCardsFollowing(suit: first.suit, game: game) return } guard !sorter.hasTrump(in: cards) else { // Must follow with trump handCards = cards.map { .init(card: $0, isPlayable: sorter.isTrump($0)) } return } // Can play any card if not in calling game guard let suit = game.calledSuit else { setAllCards(playable: true) return } // Can play any card, except the called ace let ace = Card(suit, .ass) handCards = cards.map { .init(card: $0, isPlayable: $0 != ace) } } private func setPlayableCardsFollowing(suit: Card.Suit, game: GameType) { let cards = rawCards let sorter = game.sortingType // No calling game, allow all cards of same suit let suitCards = sorter.cards(with: suit, in: cards) func followSuit() { handCards = cards.map { .init(card: $0, isPlayable: !sorter.isTrump($0) && $0.suit == suit) } } guard let called = game.calledSuit else { if suitCards.isEmpty { // Can play any card setAllCards(playable: true) } else { // Must follow suit followSuit() } return } let ace = Card(called, .ass) guard called == suit else { if suitCards.isEmpty { // Exclude called ace, all others allowed handCards = cards.map { .init(card: $0, isPlayable: $0 != ace) } } else { // Must follow suit (called ace automatically excluded) followSuit() } return } // The called suit is player, must commit ace guard cards.contains(ace) else { // Must follow suit followSuit() return } // Must play ace handCards = cards.map { .init(card: $0, isPlayable: $0 == ace) } } private func setPlayableCardsForStarter(game: GameType) { guard let suit = game.calledSuit else { setAllCards(playable: true) return } let cards = rawCards let ace = Card(suit, .ass) // Check if called ace exists, to prohibit other cards of the same suit guard cards.contains(ace) else { setAllCards(playable: true) return } // Jodeln if cards.count == numberOfCardsPerPlayer, cards.suitCount(suit, in: game) >= numberOfCardsToProtectAce { setAllCards(playable: true) return } // Only ace allowed for the called suit handCards = cards.map { card in let notPlayable = card.suit == suit && !card.symbol.isTrumpOrAce return PlayableCard(card: card, isPlayable: !notPlayable) } } private func setAllCards(playable: Bool) { for i in 0.. PlayerInfo { .init(player: self, isMasked: masked, trickPosition: positionInTrick) } } extension Player { /// Indicate that the player is connected when at a table var isConnected: Bool { guard let socket = socket else { return false } guard !socket.isClosed else { self.socket = nil return false } return true } } extension Player: Equatable { static func == (lhs: Player, rhs: Player) -> Bool { lhs.name == rhs.name } }