From cdf079698eb36a5ff4965784ea769e2399986d96 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 1 Dec 2021 22:49:54 +0100 Subject: [PATCH] Add card dealing --- Sources/App/Infos/CardInfo.swift | 17 +- Sources/App/Infos/GameType.swift | 34 +- Sources/App/Infos/TableInfo.swift | 20 +- Sources/App/Model/Game.swift | 71 +++++ Sources/App/Model/GamePhase.swift | 22 ++ Sources/App/Model/Trick.swift | 16 + Sources/App/Results/DealCardsResult.swift | 15 + .../{Infos => Results}/JoinTableResult.swift | 0 Sources/App/Sorting/Dealer.swift | 298 ++++++++++++++++++ Sources/App/routes.swift | 14 + 10 files changed, 499 insertions(+), 8 deletions(-) create mode 100644 Sources/App/Model/Game.swift create mode 100644 Sources/App/Model/GamePhase.swift create mode 100644 Sources/App/Model/Trick.swift create mode 100644 Sources/App/Results/DealCardsResult.swift rename Sources/App/{Infos => Results}/JoinTableResult.swift (100%) create mode 100644 Sources/App/Sorting/Dealer.swift diff --git a/Sources/App/Infos/CardInfo.swift b/Sources/App/Infos/CardInfo.swift index d6bb463..44b36b4 100644 --- a/Sources/App/Infos/CardInfo.swift +++ b/Sources/App/Infos/CardInfo.swift @@ -1,10 +1,19 @@ import Foundation -struct CardInfo: Codable { +struct CardInfo: ClientMessage { + + static let type: ClientMessageType = .cardInfo + + struct HandCard: Codable { + + let card: CardId + + let playable: Bool + } /// The cards for a player - let cards: [CardId] + let cards: [HandCard] - /// Indicates if the card can be played - let playable: [Bool] + // The cards on the table, as seen from the players perspective + let tableCards: [CardId] } diff --git a/Sources/App/Infos/GameType.swift b/Sources/App/Infos/GameType.swift index 1aa50d5..b3df7a6 100644 --- a/Sources/App/Infos/GameType.swift +++ b/Sources/App/Infos/GameType.swift @@ -1,6 +1,6 @@ import Foundation -enum GameType { +enum GameType: Codable { case rufEichel case rufBlatt @@ -52,5 +52,37 @@ enum GameType { return 20 } } + + var sortingType: CardSortingStrategy { + switch self { + case .wenz: + return .wenz + case .geier: + return .geier + case .soloEichel: + return .soloEichel + case .soloBlatt: + return .soloBlatt + case .soloSchelln: + return .soloSchelln + default: + return .normal + } + } } +enum CardSortingStrategy { + + /// The sorting for most games, where heart is trump + case normal + + case wenz + + case geier + + case soloEichel + + case soloBlatt + + case soloSchelln +} diff --git a/Sources/App/Infos/TableInfo.swift b/Sources/App/Infos/TableInfo.swift index f396f82..ab61c06 100644 --- a/Sources/App/Infos/TableInfo.swift +++ b/Sources/App/Infos/TableInfo.swift @@ -1,14 +1,28 @@ import Foundation -struct TableInfo: Codable { +struct TableInfo: ClientMessage { + + static let type: ClientMessageType = .tableInfo let id: String let name: String - var players: [PlayerName] + let players: [PlayerState] - var connected: [Bool] + let tableIsFull: Bool + + struct PlayerState: Codable, Equatable { + + let name: PlayerName + + let connected: Bool + + init(name: PlayerName, connected: Bool) { + self.name = name + self.connected = connected + } + } } extension TableInfo: Comparable { diff --git a/Sources/App/Model/Game.swift b/Sources/App/Model/Game.swift new file mode 100644 index 0000000..ba9e42e --- /dev/null +++ b/Sources/App/Model/Game.swift @@ -0,0 +1,71 @@ +import Foundation + +typealias Hand = [Card] + +/// The number of tricks ("Stich") per game +let tricksPerGame = 8 + +struct Game: Codable { + + let type: GameType + + let numberOfDoubles: Int + + /// The player(s) leading the game, i.e. they lose on 60-60 + let leaders: [Int] + + /// The remaining cards for all players + var cards: [Hand] + + /// The total number of consecutive trump cards for one party (starts counting at 3) + let consecutiveTrumps: Int + + var lastTrickWinner: Int + + var currentActor: Int + + /// The finished tricks, with each trick sorted according to the player order of the table + var completedTricks: [Trick] + + init(type: GameType, doubles: Int, cards: [Hand], leaders: [Int], starter: Int) { + self.type = type + self.numberOfDoubles = doubles + self.cards = cards + self.leaders = leaders + self.consecutiveTrumps = Dealer.consecutiveTrumps( + in: leaders.map { cards[$0] }.joined(), + for: type) + self.currentActor = starter + self.lastTrickWinner = starter + self.completedTricks = [] + } + + var isAtEnd: Bool { + completedTricks.count == tricksPerGame + } + + var hasNotStarted: Bool { + !cards.contains { !$0.isEmpty } + } + + var pointsOfLeaders: Int { + leaders.map(pointsOfPlayer).reduce(0, +) + } + + func pointsOfPlayer(index: Int) -> 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 new file mode 100644 index 0000000..82eb620 --- /dev/null +++ b/Sources/App/Model/GamePhase.swift @@ -0,0 +1,22 @@ +import Foundation + +/** + The phase of a table + */ +enum GamePhase: String, Codable { + + /// The table is not yet full, so no game can be started + case waitingForPlayers = "waiting" + + /// The players are specifying if they want to double ("legen") + case collectingDoubles = "doubles" + + /// The game negotiation is ongoing + case bidding = "bidding" + + /// The game is in progress + case playing = "play" + + /// The game is over + case gameFinished = "done" +} diff --git a/Sources/App/Model/Trick.swift b/Sources/App/Model/Trick.swift new file mode 100644 index 0000000..07a5147 --- /dev/null +++ b/Sources/App/Model/Trick.swift @@ -0,0 +1,16 @@ +import Foundation + +typealias Trick = [Card] + +extension Trick { + + func winnerIndex(forGameType type: GameType) -> Int { + let highCard = Dealer.sort(cards: self, using: type.sortingType).first! + return firstIndex(of: highCard)! + } + + var points: Int { + map { $0.points } + .reduce(0, +) + } +} diff --git a/Sources/App/Results/DealCardsResult.swift b/Sources/App/Results/DealCardsResult.swift new file mode 100644 index 0000000..ca12d1f --- /dev/null +++ b/Sources/App/Results/DealCardsResult.swift @@ -0,0 +1,15 @@ +import Foundation + + +enum DealCardResult { + + case success + + case invalidToken + + case noTableJoined + + case tableNotFull + + case tableStateInvalid +} diff --git a/Sources/App/Infos/JoinTableResult.swift b/Sources/App/Results/JoinTableResult.swift similarity index 100% rename from Sources/App/Infos/JoinTableResult.swift rename to Sources/App/Results/JoinTableResult.swift diff --git a/Sources/App/Sorting/Dealer.swift b/Sources/App/Sorting/Dealer.swift new file mode 100644 index 0000000..e251519 --- /dev/null +++ b/Sources/App/Sorting/Dealer.swift @@ -0,0 +1,298 @@ +import Foundation + +struct Dealer { + + private static let normalCardOrder = [ + 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(.herz, .ass), + Card(.herz, .zehn), + Card(.herz, .könig), + Card(.herz, .neun), + Card(.herz, .acht), + Card(.herz, .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(.schelln, .ass), + Card(.schelln, .zehn), + Card(.schelln, .könig), + Card(.schelln, .neun), + Card(.schelln, .acht), + Card(.schelln, .sieben), + ] + + private static 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 static 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 static 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 static 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 static 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 static let normalSortIndex: [Card : Int] = { + normalCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + }() + + private static let wenzSortIndex: [Card : Int] = { + wenzCardOder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + }() + + private static let geierSortIndex: [Card : Int] = { + geierCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + }() + + private static let eichelSortIndex: [Card : Int] = { + eichelCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + }() + + private static let blattSortIndex: [Card : Int] = { + blattCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + }() + + private static let schellnSortIndex: [Card : Int] = { + schellnCardOrder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + }() + + static func sort(cards: T, using strategy: CardSortingStrategy = .normal) -> [Card] where T: Sequence, T.Element == Card { + switch strategy { + case .normal: + return cards.sorted { normalSortIndex[$0]! < normalSortIndex[$1]! } + case .wenz: + return cards.sorted { wenzSortIndex[$0]! < wenzSortIndex[$1]! } + case .geier: + return cards.sorted { geierSortIndex[$0]! < geierSortIndex[$1]! } + case .soloEichel: + return cards.sorted { eichelSortIndex[$0]! < eichelSortIndex[$1]! } + case .soloBlatt: + return cards.sorted { blattSortIndex[$0]! < blattSortIndex[$1]! } + case .soloSchelln: + return cards.sorted { schellnSortIndex[$0]! < schellnSortIndex[$1]! } + } + } + + /** + Creates a random assignment of 8 cards per 4 players. + */ + static func deal() -> [[Card]] { + let deck = Card.Suit.allCases.map { suit in + Card.Symbol.allCases.map { symbol in + Card(suit: suit, symbol: symbol) + } + }.joined() + let random = Array(deck).shuffled() + return (0..<4).map { part -> Array.SubSequence in + let start = part * 8 + let end = start + 8 + return random[start..(in cards: T, for game: GameType) -> Int where T: Sequence, T.Element == Card { + 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 cards.contains(trumpsInOrder[count]) { + count += 1 + } + guard count >= 3 else { + return 0 + } + return count + } +} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 72f2aad..eb0341a 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -264,4 +264,18 @@ func routes(_ app: Application) throws { } return "" } + + app.post("deal") { req -> String in + guard let token = req.body.string else { + throw Abort(.badRequest) + } + switch database.dealCards(playerToken: token) { + case .success: + return "" + case .invalidToken: + throw Abort(.unauthorized) // 401 + default: + throw Abort(.preconditionFailed) // 412 + } + } }