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(hasWedding: Bool) { isNextActor = true actions = [.increaseOrMatchGame, .withdrawFromAuction] if hasWedding { actions.append(.acceptWedding) } } 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 handCards = game.sortingType.sort(rawCards).map { .init(card: $0, isPlayable: 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 didFinish(trick: Trick, winner: Bool, canDoubleInNextRound: Bool) { isNextActor = winner //startedCurrentTrick = winner if winner { wonTricks.append(trick) } if canDoubleInNextRound, !isGameLeader { actions = [.doubleDuringGame] } else { actions = [] } } func didFinishGame() { actions = [.deal] } func clearLastTrick() { playedCard = nil // This flag is not set until the last trick is cleared, because // it would mess up the stacking of the cards on the table // which relies on this property startedCurrentTrick = isNextActor } 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 firstCard = trick.first else { setPlayableCardsForStarter(game: game) return } let sorter = game.sortingType guard sorter.isTrump(firstCard) else { setPlayableCardsFollowing(suit: firstCard.suit, game: game) return } guard !sorter.hasTrump(in: cards) else { // Must follow with trump handCards = cards.map { .init(card: $0, isPlayable: sorter.isTrump($0)) } if !handCards.contains(where: { $0.isPlayable }) { print("No cards to play when having to follow trump") } 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) } if !handCards.contains(where: { $0.isPlayable }) { print("No cards to play when not having to follow trump in a called game") } } private func setPlayableCardsFollowing(suit playedSuit: Card.Suit, game: GameType) { let cards = rawCards let sorter = game.sortingType let suitCards = sorter.cards(with: playedSuit, in: cards) func followSuit() { handCards = cards.map { .init(card: $0, isPlayable: !sorter.isTrump($0) && $0.suit == playedSuit) } if !handCards.contains(where: { $0.isPlayable }) { print("No cards to play when following suit") } } guard let calledSuit = game.calledSuit else { if suitCards.isEmpty { // Can play any card setAllCards(playable: true) } else { // Must follow suit followSuit() } return } print("Has called suit \(calledSuit)") let ace = Card(calledSuit, .ass) guard !suitCards.isEmpty else { // Exclude called ace, all others allowed handCards = cards.map { .init(card: $0, isPlayable: $0 != ace) } if !handCards.contains(where: { $0.isPlayable }) { print("No cards to play when following called suit without suit cards") } return } guard calledSuit == playedSuit else { print("Following uncalled suit since no suitable cards") // Must follow suit (called ace not present) followSuit() return } // The called suit is played, 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) } if !handCards.contains(where: { $0.isPlayable }) { print("No cards to play when having to play ace of called suit") } } 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 } }