From 20d1ce24da6273e1b6650a82e6b07eb3ba1362d2 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Mon, 6 Dec 2021 11:43:30 +0100 Subject: [PATCH] Player actions, playable cards --- Sources/App/Extensions/Array+Extensions.swift | 4 + Sources/App/Infos/CardInfo.swift | 8 + Sources/App/Management/Database.swift | 9 +- Sources/App/Management/TableManagement.swift | 13 +- Sources/App/Model/Dealer.swift | 291 +--------------- Sources/App/Model/Game.swift | 70 ---- Sources/App/Model/GamePhase.swift | 6 + Sources/App/Model/GameType.swift | 41 ++- Sources/App/Model/Player.swift | 319 ++++++++++++++++-- Sources/App/Model/Table.swift | 306 +++++++++++++++-- Sources/App/Model/Trick.swift | 16 - Sources/App/Results/PlayCardResult.swift | 14 + Sources/App/routes.swift | 25 +- 13 files changed, 675 insertions(+), 447 deletions(-) create mode 100644 Sources/App/Infos/CardInfo.swift delete mode 100644 Sources/App/Model/Game.swift delete mode 100644 Sources/App/Model/Trick.swift create mode 100644 Sources/App/Results/PlayCardResult.swift diff --git a/Sources/App/Extensions/Array+Extensions.swift b/Sources/App/Extensions/Array+Extensions.swift index be09181..87577fc 100644 --- a/Sources/App/Extensions/Array+Extensions.swift +++ b/Sources/App/Extensions/Array+Extensions.swift @@ -8,4 +8,8 @@ extension Array { } return Array(self[index..(by converting: (Element) -> T) -> [Element] where T: Comparable { + sorted { converting($0) < converting($1) } + } } diff --git a/Sources/App/Infos/CardInfo.swift b/Sources/App/Infos/CardInfo.swift new file mode 100644 index 0000000..d81277f --- /dev/null +++ b/Sources/App/Infos/CardInfo.swift @@ -0,0 +1,8 @@ +import Foundation + +struct CardInfo: Codable, Equatable { + + let card: CardId + + let playable: Bool +} diff --git a/Sources/App/Management/Database.swift b/Sources/App/Management/Database.swift index b476823..6d9686c 100644 --- a/Sources/App/Management/Database.swift +++ b/Sources/App/Management/Database.swift @@ -76,7 +76,7 @@ final class Database { - Parameter isPublic: Indicates that this is a game joinable by everyone - Returns: The table id */ - func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId { + func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo { tables.createTable(named: name, player: player, isPublic: isPublic) } @@ -105,4 +105,11 @@ final class Database { } return tables.performAction(player: player, action: action) } + + func play(card: Card, playerToken: SessionToken) -> PlayCardResult { + guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { + return .invalidToken + } + return tables.play(card: card, player: player) + } } diff --git a/Sources/App/Management/TableManagement.swift b/Sources/App/Management/TableManagement.swift index 54aeb67..1c2b36c 100644 --- a/Sources/App/Management/TableManagement.swift +++ b/Sources/App/Management/TableManagement.swift @@ -90,12 +90,12 @@ final class TableManagement: DiskWriter { - Parameter isPublic: Indicates that this is a game joinable by everyone - Returns: The table id */ - func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId { + func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo { let table = Table(newTable: name, isPublic: isPublic) - _ = table.add(player: name) + _ = table.add(player: player) tables[table.id] = table writeTableToDisk(table: table) - return table.id + return table.compileInfo(for: player)! } /// A list of all public tables @@ -171,4 +171,11 @@ final class TableManagement: DiskWriter { } return table.perform(action: action, forPlayer: player) } + + func play(card: Card, player: PlayerName) -> PlayCardResult { + guard let table = currentTable(for: player) else { + return .noTableJoined + } + return table.play(card: card, player: player) + } } diff --git a/Sources/App/Model/Dealer.swift b/Sources/App/Model/Dealer.swift index ca09e5e..b518095 100644 --- a/Sources/App/Model/Dealer.swift +++ b/Sources/App/Model/Dealer.swift @@ -1,294 +1,5 @@ import Foundation -private let wenzCardOder = [ - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .ober), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .ober), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .ober), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .ober), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), -] - -private let geierCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .unter), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .unter), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .unter), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .unter), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), -] - -private let eichelCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), -] - -private let blattCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), -] - -private let schellnCardOrder = [ - Card(.eichel, .ober), - Card(.blatt, .ober), - Card(.herz, .ober), - Card(.schelln, .ober), - Card(.eichel, .unter), - Card(.blatt, .unter), - Card(.herz, .unter), - Card(.schelln, .unter), - Card(.schelln, .ass), - Card(.schelln, .zehn), - Card(.schelln, .könig), - Card(.schelln, .neun), - Card(.schelln, .acht), - Card(.schelln, .sieben), - Card(.eichel, .ass), - Card(.eichel, .zehn), - Card(.eichel, .könig), - Card(.eichel, .neun), - Card(.eichel, .acht), - Card(.eichel, .sieben), - Card(.blatt, .ass), - Card(.blatt, .zehn), - Card(.blatt, .könig), - Card(.blatt, .neun), - Card(.blatt, .acht), - Card(.blatt, .sieben), - Card(.herz, .ass), - Card(.herz, .zehn), - Card(.herz, .könig), - Card(.herz, .neun), - Card(.herz, .acht), - Card(.herz, .sieben), -] - -private let wenzSortIndex: [Card : Int] = { - wenzCardOder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } -}() - -private let geierSortIndex: [Card : Int] = { - geierCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } -}() - -private let eichelSortIndex: [Card : Int] = { - eichelCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } -}() - -private let blattSortIndex: [Card : Int] = { - blattCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } -}() - -private let schellnSortIndex: [Card : Int] = { - schellnCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } -}() - -private let wenzTrumps: [Card] = wenzCardOder[0..<4] - -private let geierTrumps: [Card] = geierCardOrder[0..<4] - -private let eichelTrumps: [Card] = eichelCardOrder[0..<8] - -private let blattTrumps: [Card] = blattCardOrder[0..<8] - -private let schellnTrumps: [Card] = schellnCardOrder[0..<8] - -extension Card { - - func isTrump(in game: GameType) -> Bool { - switch game.sortingType { - case .normal: - trumpsInOrder = normalCardOrder[0..<8] - case .wenz: - trumpsInOrder = wenzCardOder[0..<4] - case .geier: - trumpsInOrder = geierCardOrder[0..<4] - case .soloEichel: - trumpsInOrder = eichelCardOrder[0..<8] - case .soloBlatt: - trumpsInOrder = blattCardOrder[0..<8] - case .soloSchelln: - trumpsInOrder = schellnCardOrder[0..<8] - } - } -} - -extension Array where Element == Card { - - func sorted(cardOrder order: CardOrder) -> [Card] { - switch order { - case .normal: - return sorted { normalSortIndex[$0]! < normalSortIndex[$1]! } - case .wenz: - return sorted { wenzSortIndex[$0]! < wenzSortIndex[$1]! } - case .geier: - return sorted { geierSortIndex[$0]! < geierSortIndex[$1]! } - case .soloEichel: - return sorted { eichelSortIndex[$0]! < eichelSortIndex[$1]! } - case .soloBlatt: - return sorted { blattSortIndex[$0]! < blattSortIndex[$1]! } - case .soloSchelln: - return sorted { schellnSortIndex[$0]! < schellnSortIndex[$1]! } - } - } - - func consecutiveTrumps(for game: GameType) -> Int { - var count = 0 - let trumpsInOrder: Array.SubSequence - switch game.sortingType { - case .normal: - trumpsInOrder = normalCardOrder[0..<8] - case .wenz: - trumpsInOrder = wenzCardOder[0..<4] - case .geier: - trumpsInOrder = geierCardOrder[0..<4] - case .soloEichel: - trumpsInOrder = eichelCardOrder[0..<8] - case .soloBlatt: - trumpsInOrder = blattCardOrder[0..<8] - case .soloSchelln: - trumpsInOrder = schellnCardOrder[0..<8] - } - while contains(trumpsInOrder[count]) { - count += 1 - } - guard count >= 3 else { - return 0 - } - return count - } - - func trumpCount(for game: GameType) -> Int { - - } - - /** - Split cards into chunks to assign them to players. - - Note: The array must contain a multiple of the `size` parameter - */ - func split(intoChunksOf size: Int) -> [Hand] { - stride(from: 0, to: count, by: size).map { i in - Array(self[i.. Int { - completedTricks - .filter { $0.winnerIndex(forGameType: type) == index } - .map { $0.points } - .reduce(0, +) - } - - /// The cost of the game, in cents - var cost: Int { - // TODO: Add läufer and schwarz, schneider - type.basicCost * costMultiplier - } - - var costMultiplier: Int { - 2 ^^ numberOfDoubles - } -} diff --git a/Sources/App/Model/GamePhase.swift b/Sources/App/Model/GamePhase.swift index 82eb620..c87c32c 100644 --- a/Sources/App/Model/GamePhase.swift +++ b/Sources/App/Model/GamePhase.swift @@ -13,9 +13,15 @@ enum GamePhase: String, Codable { /// The game negotiation is ongoing case bidding = "bidding" + + /// The game must be selected by the player + case selectGame = "select" /// The game is in progress case playing = "play" + + /// The player must select a card to give to the wedding offerer + case selectWeddingCard = "wedding" /// The game is over case gameFinished = "done" diff --git a/Sources/App/Model/GameType.swift b/Sources/App/Model/GameType.swift index 3facbe7..ff6aaaf 100644 --- a/Sources/App/Model/GameType.swift +++ b/Sources/App/Model/GameType.swift @@ -4,19 +4,24 @@ enum GameType: Codable { enum GameClass: Int { case ruf = 1 - case hochzeit = 2 - case bettel = 3 - case wenzGeier = 4 - case solo = 5 + case bettel = 2 + case wenzGeier = 3 + case solo = 4 var cost: Int { switch self { case .ruf: return 5 - case .hochzeit: return 10 case .bettel: return 15 case .wenzGeier, .solo: return 20 } } + + mutating func increase() { + guard self != .solo else { + return + } + self = .init(rawValue: rawValue + 1)! + } } case rufEichel @@ -33,10 +38,8 @@ enum GameType: Codable { var gameClass: GameClass { switch self { - case .rufEichel, .rufBlatt, .rufSchelln: + case .rufEichel, .rufBlatt, .rufSchelln, .hochzeit: return .ruf - case .hochzeit: - return .hochzeit case .bettel: return .bettel case .wenz, .geier: @@ -45,6 +48,28 @@ enum GameType: Codable { return .solo } } + + var isCall: Bool { + switch self { + case .rufEichel, .rufBlatt, .rufSchelln: + return true + default: + return false + } + } + + var calledSuit: Card.Suit? { + switch self { + case .rufEichel: + return .eichel + case .rufBlatt: + return .blatt + case .rufSchelln: + return .schelln + default: + return nil + } + } var isSingleGame: Bool { switch self { diff --git a/Sources/App/Model/Player.swift b/Sources/App/Model/Player.swift index 2109afe..a9b7cbf 100644 --- a/Sources/App/Model/Player.swift +++ b/Sources/App/Model/Player.swift @@ -4,6 +4,15 @@ 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 @@ -45,6 +54,14 @@ final class Player { var wonTricks: [Trick] = [] var socket: WebSocket? = nil + + var canOfferWedding: Bool { + rawCards.canOfferWedding + } + + var offersWedding = false + + var wouldAcceptWedding = false init(name: PlayerName) { self.name = name @@ -53,7 +70,23 @@ final class Player { 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 + } func connect(using socket: WebSocket) { _ = self.socket?.close() self.socket = socket @@ -75,69 +108,286 @@ final class Player { self.socket = nil return true } + + func canPerform(_ action: Action) -> Bool { + actions.contains(action) + } func prepareForFirstGame(isFirstPlayer: Bool) { playsFirstCard = isFirstPlayer isNextActor = isFirstPlayer - selectsGame = false // Not relevant in this phase startedCurrentTrick = isFirstPlayer actions = [.deal] - didDoubleAfterFourCards = nil // Not relevant in this phase - isStillBidding = false // Not relevant in this phase - isGameLeader = false // Not relevant in this phase - numberOfRaises = 0 // Not relevant in this phase handCards = [] playedCard = nil wonTricks = [] } func assignFirstCards(_ cards: Hand) { - selectsGame = false // Not relevant in this phase actions = [.initialDoubleCost, .noDoubleCost] - didDoubleAfterFourCards = nil - isStillBidding = false // Not relevant in this phase - isGameLeader = false // Not relevant in this phase - numberOfRaises = 0 // Not relevant in this phase handCards = cards.map { .init(card: $0, isPlayable: false) } - playedCard = nil - wonTricks = [] } func didDouble(_ double: Bool) { - selectsGame = false // Not relevant in this phase actions = [] didDoubleAfterFourCards = double - isStillBidding = false // Not relevant in this phase - isGameLeader = false // Not relevant in this phase - numberOfRaises = 0 // Not relevant in this phase - playedCard = nil - wonTricks = [] } func assignRemainingCards(_ cards: Hand) { - isStillBidding = true - isGameLeader = false - numberOfRaises = 0 handCards = (rawCards + cards) - .sorted(CardOrderType: .normal) + .sortedCards(order: NormalCardOrder.self) .map { .init(card: $0, isPlayable: false) } - playedCard = nil - wonTricks = [] } func startAuction() { - selectsGame = false // Not relevant in this phase if playsFirstCard { actions = [.withdrawFromAuction, .increaseOrMatchGame] } else { - + actions = [] + } + if canOfferWedding { + actions.append(.offerWedding) + } + } + + func offerWedding() { + offersWedding = true + actions = [] + } + + func weddingOfferExists() { + guard isStillBidding else { + return + } + actions = [.increaseOrMatchGame, .withdrawFromAuction] + } + + func hasWeddingOffer() { + guard isStillBidding else { + return + } + actions = [.acceptWedding, .increaseOrMatchGame, .withdrawFromAuction] + } + + func weddingOutbid() { + isNextActor = false + guard isStillBidding else { + return } actions = [] - isStillBidding = true - isGameLeader = false // Not relevant in this phase - numberOfRaises = 0 // Not relevant in this phase + if offersWedding { + offersWedding = false + isStillBidding = 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 mustSelectGame() { + isNextActor = 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 gameStarts() { + isNextActor = playsFirstCard + startedCurrentTrick = playsFirstCard + actions = [.doubleDuringGame] + isGameLeader = false + } + + func switchLeadership() { + isGameLeader.toggle() + if isGameLeader { + actions = actions.filter { $0 != .doubleDuringGame } + } else if !actions.contains(.doubleDuringGame) { + actions.append(.doubleDuringGame) + } + } + + func withdrawFromBidding() { + isStillBidding = false + actions = [] + } + + func didFinishTrick(canDoubleInNextRound: Bool) { + isNextActor = false playedCard = nil - wonTricks = [] + if canDoubleInNextRound, !isGameLeader { + actions = [.doubleDuringGame] + } + } + + 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 { @@ -193,3 +443,10 @@ extension Player { } } } + +extension Player: Equatable { + + static func == (lhs: Player, rhs: Player) -> Bool { + lhs.name == rhs.name + } +} diff --git a/Sources/App/Model/Table.swift b/Sources/App/Model/Table.swift index 631f00e..ef8e91a 100644 --- a/Sources/App/Model/Table.swift +++ b/Sources/App/Model/Table.swift @@ -22,10 +22,7 @@ final class Table { var gameType: GameType? = nil - var minimumPlayableGame: GameType.GameClass = .ruf - - /// Indicates if doubles are still allowed - var canDoubleDuringGame = false + var minimumPlayableGame: GameType.GameClass? /// Indicates if any player doubled during the current round, extending it to the next round var didDoubleInCurrentRound = false @@ -34,6 +31,42 @@ final class Table { var allPlayersFinishedDoubling: Bool { !players.contains { $0.didDoubleAfterFourCards == nil } } + + 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 @@ -63,6 +96,11 @@ final class Table { 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 } @@ -86,6 +124,29 @@ final class Table { } 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 else { + continue + } + return player + } + return player + } func remove(player: PlayerName) { guard contains(player: player) else { @@ -116,11 +177,10 @@ final class Table { } private func prepareTableForFirstGame() { - self.phase = .waitingForPlayers - self.gameType = nil - self.minimumPlayableGame = .ruf // Not relevant in this phase - self.canDoubleDuringGame = true // Not relevant in this phase - self.didDoubleInCurrentRound = false // Not relevant in this phase + 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() + 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 { + return .tableStateInvalid + } defer { sendUpdateToAllPlayers() } switch action { case .deal: @@ -149,15 +251,15 @@ final class Table { case .noDoubleCost: return perform(double: false, forPlayer: player) case .offerWedding: - fatalError() + return performWeddingCall(forPlayer: player) case .acceptWedding: - fatalError() + return handleWeddingAccept(forPlayer: player) case .increaseOrMatchGame: - fatalError() + return performBidIncrease(forPlayer: player) case .withdrawFromAuction: - fatalError() + return performWithdrawl(forPlayer: player) case .doubleDuringGame: - fatalError() + return performDoubleDuringGame(forPlayer: player) } } @@ -169,19 +271,16 @@ final class Table { return .tableStateInvalid } - phase = .collectingDoubles - gameType = nil - minimumPlayableGame = .ruf - 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 name: PlayerName) -> PlayerActionResult { - let player = select(player: player)! + func perform(double: Bool, forPlayer player: Player) -> PlayerActionResult { player.didDouble(double) if allPlayersFinishedDoubling { dealAdditionalCards() @@ -194,18 +293,173 @@ final class Table { for (index, player) in players.enumerated() { player.assignRemainingCards(cards[index]) } + players.forEach { $0.startAuction() } + minimumPlayableGame = nil + } + + private func performWeddingCall(forPlayer player: Player) -> PlayerActionResult { + guard phase == .bidding else { + return .tableStateInvalid + } + guard minimumPlayableGame == nil || minimumPlayableGame == .ruf else { + return .tableStateInvalid + } + guard player.offersWedding else { + return .tableStateInvalid + } + guard !weddingOfferExists else { + // Only one wedding allowed at the table + return .tableStateInvalid + } + // Only allow wedding acceptance or outbidding + players.forEach { $0.weddingOfferExists() } + player.offerWedding() + firstPlayer.hasWeddingOffer() + minimumPlayableGame = .bettel return .success } - - private func startAuction() { - players.forEach { $0.startAuction() } - minimumPlayableGame = .ruf + + 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() + } + 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.gameStarts() } + 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 + let first = firstPlayer + let newPlayer = self.nextBidder(after: first) + first.playsFirstCard = false + newPlayer.playsFirstCard = true + prepareTableForFirstGame() + return dealInitialCards() + default: + break + } + return .success + } + + private func selectGame(player: Player) { + minimumPlayableGame = nil + gameType = nil + phase = .selectGame + players.forEach { $0.auctionEnded() } + player.mustSelectGame() + } + + 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 = .ruf + minimumPlayableGame = nil } } diff --git a/Sources/App/Model/Trick.swift b/Sources/App/Model/Trick.swift deleted file mode 100644 index f14e0b6..0000000 --- a/Sources/App/Model/Trick.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -typealias Trick = [Card] - -extension Trick { - - func winnerIndex(forGameType type: GameType) -> Int { - let highCard = sorted(cardOrder: type.sortingType).first! - return firstIndex(of: highCard)! - } - - var points: Int { - map { $0.points } - .reduce(0, +) - } -} diff --git a/Sources/App/Results/PlayCardResult.swift b/Sources/App/Results/PlayCardResult.swift new file mode 100644 index 0000000..e245218 --- /dev/null +++ b/Sources/App/Results/PlayCardResult.swift @@ -0,0 +1,14 @@ +import Foundation + +enum PlayCardResult { + + case success + + case invalidToken + + case noTableJoined + + case invalidTableState + + case invalidCard +} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index b721380..838d6d6 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -203,8 +203,8 @@ func routes(_ app: Application) throws { guard let player = database.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } - let tableId = database.createTable(named: tableName, player: player, isPublic: isPublic) - return tableId + let table = database.createTable(named: tableName, player: player, isPublic: isPublic) + return try encodeJSON(table) } /** @@ -293,4 +293,25 @@ func routes(_ app: Application) throws { throw Abort(.preconditionFailed) // 412 } } + + app.post("player", "card", ":card") { req -> String in + guard let token = req.body.string, + let cardId = req.parameters.get("card"), + let card = Card(id: cardId) else { + throw Abort(.badRequest) + } + switch database.play(card: card, playerToken: token) { + case .success: + return "" + case .invalidToken: + throw Abort(.unauthorized) // 401 + case .noTableJoined: + throw Abort(.preconditionFailed) // 412 + case .invalidTableState: + throw Abort(.preconditionFailed) // 412 + case .invalidCard: + throw Abort(.preconditionFailed) // 412 + + } + } }