import Foundation import WebSocketKit private extension Int { mutating func advanceInTable() { self = (self + 1) % maximumPlayersPerTable } } final class Table { let id: TableId let name: TableName let isPublic: Bool var players: [Player] = [] var phase: GamePhase = .waitingForPlayers var gameType: GameType? = nil var minimumPlayableGame: GameType.GameClass? /// Indicates if any player doubled during the current round, extending it to the next round var didDoubleInCurrentRound = false /// Indicates that all players acted after the first four cards var allPlayersFinishedDoubling: Bool { !players.contains { $0.didDoubleAfterFourCards == nil } } /// At least one double exists after all players acted on their first cards var initialDoubleExists: Bool { players.contains { $0.didDoubleAfterFourCards == true } } var weddingOfferExists: Bool { players.contains { $0.offersWedding } } var weddingAcceptExists: Bool { players.contains { $0.wouldAcceptWedding } } var hasAuctionWinner: Bool { numberOfRemainingBidders == 1 } var numberOfRemainingBidders: Int { players.filter { $0.isStillBidding }.count } var auctionWinner: Player { players.first { $0.isStillBidding }! } var hasCompletedTrick: Bool { !players.contains { $0.playedCard == nil } } var completedTrick: Trick? { let trick = players.compactMap { $0.playedCard } guard trick.count == maximumPlayersPerTable else { return nil } return trick } var currentTrick: [Card] { players.compactMap { $0.playedCard } } var didFinishGame: Bool { !players.contains { !$0.handCards.isEmpty } } init(id: TableId, name: TableName, isPublic: Bool) { self.id = id self.name = name self.isPublic = isPublic } init(newTable name: TableName, isPublic: Bool) { self.id = .newToken() self.name = name self.isPublic = isPublic } func add(player: PlayerName) -> Bool { guard !isFull else { return false } let player = Player(name: player) players.append(player) if isFull { prepareTableForFirstGame() } sendUpdateToAllPlayers() return true } func contains(player: PlayerName) -> Bool { players.contains { $0.name == player } } /// The player to play the first card of the current game var firstPlayer: Player { players.first { $0.playsFirstCard }! } func select(player: PlayerName) -> Player? { players.first { $0.name == player } } var indexOfTrickStarter: Int { players.firstIndex { $0.startedCurrentTrick }! } func playerInfo(at index: Int, masked: Bool) -> PlayerInfo? { let starter = indexOfTrickStarter let layer = (4 - starter + index) % 4 return player(at: index)?.info(masked: masked, positionInTrick: layer) } func playerInfo(leftOf index: Int, masked: Bool) -> PlayerInfo? { playerInfo(at: (index + 1) % 4, masked: masked) } func playerInfo(acrossOf index: Int, masked: Bool) -> PlayerInfo? { playerInfo(at: (index + 2) % 4, masked: masked) } func playerInfo(rightOf index: Int, masked: Bool) -> PlayerInfo? { playerInfo(at: (index + 3) % 4, masked: masked) } func player(at index: Int) -> Player? { guard index < players.count else { return nil } return players[index] } func index(of player: Player) -> Int { players.firstIndex(of: player)! } func nextPlayer(after player: Player) -> Player { let i = index(of: player) let newIndex = (i + 1) % maximumPlayersPerTable return players[newIndex] } func nextBidder(after player: Player) -> Player { // Find next player to place bid let index = index(of: player) for i in 1..<4 { let player = players[(index + i) % 4] guard player.isStillBidding, !player.offersWedding else { continue } return player } return player } func remove(player: PlayerName) { guard let index = players.firstIndex(where: { $0.name == player }) else { return } let removedPlayer = players[index] if removedPlayer.playsFirstCard { players[(index + 1) % players.count].playsFirstCard = true } players.remove(at: index) reset() } func connect(player name: PlayerName, using socket: WebSocket) -> Bool { guard let player = select(player: name) else { return false } player.connect(using: socket) sendUpdateToAllPlayers() return true } func disconnect(player name: PlayerName) { guard let player = select(player: name) else { return } guard player.disconnect() else { return } sendUpdateToAllPlayers() return } private func prepareTableForFirstGame() { phase = .waitingForPlayers gameType = nil minimumPlayableGame = nil // Not relevant in this phase didDoubleInCurrentRound = false // Not relevant in this phase let index = players.firstIndex { $0.playsFirstCard } ?? 0 for i in 0.. PlayCardResult { let player = select(player: name)! if phase == .selectWeddingCard { return selectedCardForWedding(card: card, player: player) } guard let game = gameType, player.hasPlayable(card: card) else { // Player only has playable cards when it is active return .invalidCard } if hasCompletedTrick { // Hide cards from last trick when next card is played players.forEach { $0.clearLastTrick() } } player.play(card: card) if let completedTrick = completedTrick { didFinish(trick: completedTrick, in: game) // Update cards for empty trick players.forEach { $0.setPlayableCards(forCurrentTrick: [], in: game) } } else { let next = nextPlayer(after: player) next.isNextActor = true player.isNextActor = false // Update cards for empty trick players.forEach { $0.setPlayableCards(forCurrentTrick: currentTrick, in: game) } } if didFinishGame { finishedGame() } sendUpdateToAllPlayers() return .success } private func finishedGame() { phase = .gameFinished players.forEach { $0.didFinishGame() } guard didFinishGame else { // Either no doubles or bids return } // TODO: Calculate winner, points, cost } func didFinish(trick: Trick, in game: GameType) { // If trick is completed, calculate winner let startIndex = indexOfTrickStarter let rotated = trick.rotated(toStartAt: startIndex) let index = rotated.highCardIndex(forGame: game) print("Winner \(index) for \(rotated)") let winner = players[(startIndex + index) % 4] players.forEach { $0.didFinish(trick: trick, winner: winner == $0, canDoubleInNextRound: didDoubleInCurrentRound) } if game == .bettel && winner.isGameLeader { // A bettel is lost if a single trick is won by the leader finishedGame() return } didDoubleInCurrentRound = false } func perform(action: Player.Action, forPlayer player: PlayerName) -> PlayerActionResult { let player = select(player: player)! guard player.canPerform(action) else { print("Player \(player) wants to \(action.path), but only allowed: \(player.actions)") return .tableStateInvalid } defer { sendUpdateToAllPlayers() } switch action { case .deal: return dealInitialCards() case .initialDoubleCost: return perform(double: true, forPlayer: player) case .noDoubleCost: return perform(double: false, forPlayer: player) case .offerWedding: return performWeddingCall(forPlayer: player) case .acceptWedding: return handleWeddingAccept(forPlayer: player) case .increaseOrMatchGame: return performBidIncrease(forPlayer: player) case .withdrawFromAuction: return performWithdrawl(forPlayer: player) case .doubleDuringGame: return performDoubleDuringGame(forPlayer: player) } } private func dealInitialCards() -> PlayerActionResult { guard isFull else { return .tableNotFull } guard phase == .waitingForPlayers || phase == .gameFinished else { return .tableStateInvalid } if phase == .gameFinished { prepareForNextGame() } let cards = Dealer.dealFirstCards() for (index, player) in players.enumerated() { player.assignFirstCards(cards[index]) } phase = .collectingDoubles gameType = nil return .success } func perform(double: Bool, forPlayer player: Player) -> PlayerActionResult { player.didDouble(double) guard allPlayersFinishedDoubling else { return .success } if initialDoubleExists { dealAdditionalCards() } else { finishedGame() } return .success } private func dealAdditionalCards() { let cards = Dealer.dealRemainingCards(of: players.map { $0.rawCards }) for (index, player) in players.enumerated() { player.assignRemainingCards(cards[index]) } players.forEach { $0.startAuction() } minimumPlayableGame = nil phase = .bidding } private func performWeddingCall(forPlayer player: Player) -> PlayerActionResult { guard phase == .bidding else { print("Invalid phase \(phase) for wedding call") return .tableStateInvalid } guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else { print("Invalid minimum game \(minimumPlayableGame!) for wedding call") return .tableStateInvalid } guard player.canOfferWedding else { print("Player does not offer wedding") return .tableStateInvalid } guard !weddingOfferExists else { // Only one wedding allowed at the table print("Already one wedding at table") return .tableStateInvalid } // Only allow wedding acceptance or outbidding players.forEach { $0.weddingOfferExists() } player.offerWedding() firstPlayer.hasWeddingOffer() minimumPlayableGame = .bettel return .success } private func performBidIncrease(forPlayer player: Player) -> PlayerActionResult { guard phase == .bidding else { return .tableStateInvalid } if weddingOfferExists { // Anyone except the offerer can outbid a wedding return handleWeddingOutbid(forPlayer: player) } guard player.isNextActor else { return .tableStateInvalid } if minimumPlayableGame == nil { minimumPlayableGame = .ruf } else { minimumPlayableGame!.increase() // Remove wedding offers players.forEach { $0.weddingOutbid() } } player.didPerformBid() if numberOfRemainingBidders == 1 { selectGame(player: player) } // Find next player to place bid nextBidder(after: player).requiresBid(hasWedding: false) return .success } private func handleWeddingOutbid(forPlayer player: Player) -> PlayerActionResult { if player.offersWedding { // A player offering a wedding can't outbid itself return .tableStateInvalid } players.forEach { $0.weddingOutbid() } nextBidder(after: player).requiresBid(hasWedding: true) return .success } private func handleWeddingAccept(forPlayer player: Player) -> PlayerActionResult { guard phase == .bidding else { return .tableStateInvalid } guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else { return .tableStateInvalid } guard weddingOfferExists else { return .tableStateInvalid } guard player.isNextActor else { return .tableStateInvalid } guard !player.offersWedding else { return .tableStateInvalid } guard !weddingAcceptExists else { return .tableStateInvalid } if hasAuctionWinner { selectedWedding(player: player) return .success } minimumPlayableGame = .bettel players.forEach { $0.weddingAccepted() } player.acceptWedding() nextBidder(after: player).requiresBid(hasWedding: false) return .success } private func selectedWedding(player: Player) { minimumPlayableGame = nil gameType = .hochzeit phase = .selectWeddingCard players.forEach { $0.auctionEnded() } player.mustSelectWeddingCard() } private func selectedCardForWedding(card: Card, player: Player) -> PlayCardResult { guard player.isNextActor, player.wouldAcceptWedding, weddingOfferExists else { return .invalidTableState } guard !card.isTrump(in: .hochzeit), player.has(card: card) else { return .invalidCard } // Swap the cards let offerer = players.first { $0.offersWedding }! let offeredCard = offerer.replaceWeddingCard(with: card) player.replace(card: card, with: offeredCard) // Start the game gameType = .hochzeit players.forEach { $0.start(game: .hochzeit) } player.switchLeadership() offerer.switchLeadership() return .success } private func performWithdrawl(forPlayer player: Player) -> PlayerActionResult { guard phase == .bidding, player.isNextActor, player.isStillBidding else { return .tableStateInvalid } player.withdrawFromBidding() switch numberOfRemainingBidders { case 1: if minimumPlayableGame != nil { // Will only be called when at least one player placed a bid selectGame(player: auctionWinner) return .success } case 0: // All players withdrawn, deal new cards finishedGame() return .success default: break } nextBidder(after: player).requiresBid(hasWedding: weddingOfferExists) return .success } private func prepareForNextGame() { let first = firstPlayer let newPlayer = self.nextBidder(after: first) first.playsFirstCard = false newPlayer.playsFirstCard = true print("Made \(newPlayer.name) to new starter") prepareTableForFirstGame() } private func selectGame(player: Player) { gameType = nil phase = .selectGame players.forEach { $0.auctionEnded() } player.mustSelectGame() } func select(game: GameType, player: PlayerName) -> PlayerActionResult { let player = select(player: player)! guard phase == .selectGame, player.selectsGame, game != .hochzeit else { return .tableStateInvalid } guard minimumPlayableGame == nil || game.gameClass >= minimumPlayableGame! else { return .tableStateInvalid } defer { sendUpdateToAllPlayers() } guard let suit = game.calledSuit else { phase = .playing gameType = game minimumPlayableGame = nil players.forEach { $0.start(game: game) } player.switchLeadership() return .success } guard player.canPlay(game: game) else { return .tableStateInvalid } phase = .playing gameType = game minimumPlayableGame = nil players.forEach { $0.start(game: game) } player.switchLeadership() // Find called player let ace = Card(suit, .ass) players.first { $0.rawCards.contains(ace) }!.switchLeadership() return .success } private func performDoubleDuringGame(forPlayer player: Player) -> PlayerActionResult { guard phase == .playing, !player.isGameLeader else { return .tableStateInvalid } player.numberOfRaises += 1 players.forEach { $0.switchLeadership() } didDoubleInCurrentRound = true return .success } private func reset() { phase = .waitingForPlayers gameType = nil minimumPlayableGame = nil for player in players { player.prepareForNewGame(isFirstPlayer: player.playsFirstCard) } } } extension Table { var isFull: Bool { players.count == maximumPlayersPerTable } var publicInfo: PublicTableInfo { .init(id: id, name: name, players: playerNames) } var playerNames: [PlayerName] { players.map { $0.name } } func compileInfo(for player: PlayerName) -> TableInfo? { guard let index = players.firstIndex(where: { $0.name == player }) else { return nil } return TableInfo(self, forPlayerAt: index) } }