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 } } 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 } } func playerInfo(at index: Int, masked: Bool) -> PlayerInfo? { let position = players.firstIndex { $0.startedCurrentTrick }! let layer = (index - position + 4) % 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 { return .invalidCard } player.play(card: card) if let completedTrick = completedTrick { didFinish(trick: completedTrick, in: game) } else { let next = nextPlayer(after: player) next.isNextActor = true player.isNextActor = false } updatePlayableCards() sendUpdateToAllPlayers() return .success } private func updatePlayableCards() { let playedCards = currentTrick players.forEach { $0.setPlayableCards(forCurrentTrick: playedCards, in: gameType) } } func didFinish(trick: Trick, in game: GameType) { // If trick is completed, calculate winner let index = trick.highCardIndex(forGame: game) players.forEach { $0.didFinishTrick(canDoubleInNextRound: didDoubleInCurrentRound) } players[index].didWin(trick: trick) 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 else { return .tableStateInvalid } 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 } guard initialDoubleExists else { return dealNextGame() } dealAdditionalCards() 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.offersWedding 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() // Find next player to place bid nextBidder(after: player).requiresBid() 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() } firstPlayer.requiresBid() 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() 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: selectGame(player: auctionWinner) case 0: // All players withdrawn, deal new cards return dealNextGame() default: nextBidder(after: player).requiresBid() } return .success } private func dealNextGame() -> PlayerActionResult { let first = firstPlayer let newPlayer = self.nextBidder(after: first) first.playsFirstCard = false newPlayer.playsFirstCard = true prepareTableForFirstGame() return dealInitialCards() } private func selectGame(player: Player) { minimumPlayableGame = nil 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() } 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) } }