diff --git a/Public/elements.js b/Public/elements.js
index e57de05..6e73e81 100644
--- a/Public/elements.js
+++ b/Public/elements.js
@@ -6,6 +6,11 @@ var playerName = ""
var debugSessionToken = null
const debugMode = true // Does not load session token, to allow multiple players per browser
+const playerCardsElement = "player-cards"
+
+const offlineText = "Offline"
+const missingPlayerText = "Leer"
+
function showDebugLogins() {
document.getElementById("login-window-inner").innerHTML +=
"" +
@@ -35,7 +40,7 @@ function showLoginElements() {
hide("table-list")
hide("game-bar")
hide("table-players")
- hide("player-cards")
+ hide(playerCardsElement)
}
function showTableListElements() {
@@ -45,7 +50,7 @@ function showTableListElements() {
setDisplayStyle("table-list", "inherit")
hide("game-bar")
hide("table-players")
- hide("player-cards")
+ hide(playerCardsElement)
}
function showGameElements() {
@@ -55,7 +60,7 @@ function showGameElements() {
hide("table-list")
setDisplayStyle("game-bar", "grid")
setDisplayStyle("table-players", "grid")
- setDisplayStyle("player-cards", "grid")
+ setDisplayStyle(playerCardsElement, "grid")
}
function showTableName(name) {
@@ -63,11 +68,11 @@ function showTableName(name) {
}
function showConnectedState() {
- showConnectionState("bottom", true)
+ showPlayerState("bottom", "")
}
function showDisconnectedState() {
- showConnectionState("bottom", false)
+ showPlayerDisconnected("bottom")
}
function showDealButton() {
@@ -138,26 +143,36 @@ function setTableListContent(content) {
document.getElementById("table-list").innerHTML = content
}
-function setTablePlayerInfo(position, name, connected, active, card, layer) {
- nameColor = active ? "var(--button-color)" : "var(--text-color)"
- setTablePlayerElements(position, name, nameColor, connected, card, layer)
-}
-
function setEmptyPlayerInfo(position) {
- setTablePlayerElements(position, "Empty", "var(--secondary-text-color)", true, "", 1)
+ setTablePlayerName(position, null, false)
+ showPlayerState(position, "")
+ setTableCard(position, "", 1)
}
-function setTablePlayerElements(position, name, nameColor, connected, card, layer) {
+function setTablePlayerName(position, name, active) {
const nameElement = document.getElementById("table-player-name-" + position)
- nameElement.style.color = nameColor
- nameElement.innerHTML = name
- showConnectionState(position, connected)
- setTableCard(position, card, layer)
+ if (name == null) {
+ nameElement.style.color = "var(--secondary-text-color)"
+ nameElement.innerHTML = missingPlayerText
+ } else {
+ nameElement.style.color = active ? "var(--button-color)" : "var(--text-color)"
+ nameElement.innerHTML = name
+ }
}
-function showConnectionState(position, connected) {
+function showPlayerDisconnected(position) {
+ setPlayerState(position, "var(--alert-color)", offlineText)
+}
+
+function showPlayerState(position, state) {
+ setPlayerState(position, "var(--secondary-text-color)", state)
+}
+
+function setPlayerState(position, color, text) {
const connectionElement = "table-player-state-" + position
- setDisplayStyle(connectionElement, connected ? "none" : "inherit")
+ const element = document.getElementById(connectionElement)
+ element.style.color = color
+ element.innerHTML = text
}
function setTableCard(position, card, layer) {
@@ -206,9 +221,14 @@ function updateTableInfo(table) {
setHandCard(i+1, "", false)
}
+ let playedGame = null
+ if (table.hasOwnProperty("game")) {
+ playedGame = textForAction(table.game)
+ }
+
// Show player info
console.log(table)
- setInfoForPlayer(table.player, "bottom")
+ setInfoForPlayer(table.player, "bottom", playedGame)
if (table.playerSelectsGame) {
setActionsForOwnPlayer(table.playableGames)
showAvailableGames([])
@@ -221,29 +241,61 @@ function updateTableInfo(table) {
setActionsForOwnPlayer(table.actions)
}
if (table.hasOwnProperty("playerLeft")) {
- setInfoForPlayer(table.playerLeft, "left")
+ setInfoForPlayer(table.playerLeft, "left", playedGame)
} else {
setEmptyPlayerInfo("left")
}
if (table.hasOwnProperty("playerAcross")) {
- setInfoForPlayer(table.playerAcross, "top")
+ setInfoForPlayer(table.playerAcross, "top", playedGame)
} else {
setEmptyPlayerInfo("top")
}
if (table.hasOwnProperty("playerRight")) {
- setInfoForPlayer(table.playerRight, "right")
+ setInfoForPlayer(table.playerRight, "right", playedGame)
} else {
setEmptyPlayerInfo("right")
}
}
-function setInfoForPlayer(player, position) {
+function setInfoForPlayer(player, position, game) {
var card = ""
- if (player.hasOwnProperty("playedCard")) {
- card = player.playedCard
+ if (player.hasOwnProperty("card")) {
+ card = player.card
}
+ const leadsGame = player.leads
const layer = player.position
- setTablePlayerInfo(position, player.name, player.connected, player.active, card, layer)
+ setTableCard(position, card, layer)
+ setTablePlayerName(position, player.name, player.active)
+ if (!player.connected) {
+ showPlayerDisconnected(position)
+ return
+ }
+ var state = []
+ if (game != null && leadsGame) {
+ state.push(game)
+ }
+
+ const double = doubleText(player.doubles)
+ if (double) {
+ state.push(double)
+ }
+
+ if (game != null) {
+ state.push(player.points.toString() + " Punkte")
+ }
+
+ const text = state.join(", ")
+ showPlayerState(position, text)
+}
+
+function doubleText(doubles) {
+ if (doubles == 0) {
+ return null
+ }
+ if (doubles == 1) {
+ return "gedoppelt"
+ }
+ return doubles.toString() + "x gedoppelt"
}
function clearInfoForPlayer(position) {
@@ -296,6 +348,8 @@ function textForAction(action) {
case "raise":
return "Schießen"
+ case "ruf":
+ return "Ruf"
case "ruf-eichel":
return "Ruf Eichel"
case "ruf-blatt":
@@ -308,6 +362,8 @@ function textForAction(action) {
return "Wenz"
case "geier":
return "Geier"
+ case "solo":
+ return "Solo"
case "solo-eichel":
return "Eichel Solo"
case "solo-blatt":
diff --git a/Readme.md b/Readme.md
index 721a098..f4180d5 100644
--- a/Readme.md
+++ b/Readme.md
@@ -94,5 +94,7 @@ Version 3:
- Save data persistently
- Table administrator can remove players
+# Bugs
+- Correctly show available games
diff --git a/Sources/App/Extensions/Array+Extensions.swift b/Sources/App/Extensions/Array+Extensions.swift
index 87577fc..b53f281 100644
--- a/Sources/App/Extensions/Array+Extensions.swift
+++ b/Sources/App/Extensions/Array+Extensions.swift
@@ -1,6 +1,27 @@
import Foundation
extension Array {
+
+ func at(_ index: Int) -> Element? {
+ guard index < count else {
+ return nil
+ }
+ return self[index]
+ }
+
+ func rotatedByOne() -> [Element] {
+ guard !isEmpty else {
+ return []
+ }
+ return self[1...] + [self[0]]
+ }
+
+ mutating func rotateByOne() {
+ guard !isEmpty else {
+ return
+ }
+ append(removeFirst())
+ }
func rotated(toStartAt index: Int) -> [Element] {
guard index != 0 else {
@@ -13,3 +34,35 @@ extension Array {
sorted { converting($0) < converting($1) }
}
}
+
+extension Array where Element: Equatable {
+
+ func index(of element: Element) -> Index {
+ firstIndex(of: element)!
+ }
+}
+
+extension Array where Element: Player {
+
+ var names: [PlayerName] {
+ map { $0.name }
+ }
+
+ func index(of player: PlayerName) -> Int {
+ firstIndex { $0.name == player }!
+ }
+
+ func player(named name: PlayerName) -> Element? {
+ first { $0.name == name }
+ }
+
+ func contains(player: PlayerName) -> Bool {
+ contains { $0.name == player }
+ }
+
+ func next(after player: Element) -> Element {
+ let i = index(of: player)
+ let newIndex = (i + 1) % maximumPlayersPerTable
+ return self[newIndex]
+ }
+}
diff --git a/Sources/App/Infos/PlayerInfo.swift b/Sources/App/Infos/PlayerInfo.swift
index 3ee3608..59aa25b 100644
--- a/Sources/App/Infos/PlayerInfo.swift
+++ b/Sources/App/Infos/PlayerInfo.swift
@@ -17,12 +17,22 @@ struct PlayerInfo: Codable, Equatable {
/// The height of the player card on the table stack
let positionInTrick: Int
- init(player: Player, isNextActor: Bool, position: Int) {
+ /// The number of times the player doubled the game cost (initial double and raises)
+ let numberOfDoubles: Int
+
+ let leadsGame: Bool
+
+ let points: Int?
+
+ init(player: Player, position: Int) {
self.name = player.name
self.isConnected = player.isConnected
- self.isNextActor = isNextActor
+ self.isNextActor = player.isNextActor
self.positionInTrick = position
self.playedCard = player.playedCard?.id
+ self.numberOfDoubles = player.numberOfDoubles
+ self.leadsGame = player.leadsGame
+ self.points = player.points
}
/// Convert the property names into shorter strings for JSON encoding
@@ -32,5 +42,8 @@ struct PlayerInfo: Codable, Equatable {
case isNextActor = "active"
case playedCard = "card"
case positionInTrick = "position"
+ case numberOfDoubles = "doubles"
+ case leadsGame = "leads"
+ case points = "points"
}
}
diff --git a/Sources/App/Infos/TableInfo.swift b/Sources/App/Infos/TableInfo.swift
index fcf5840..2a000bd 100644
--- a/Sources/App/Infos/TableInfo.swift
+++ b/Sources/App/Infos/TableInfo.swift
@@ -23,23 +23,22 @@ struct TableInfo: Codable {
let actions: [ActionId]
let playerSelectsGame: Bool
+
+ let game: GameId?
- init(id: String, name: String,
- own: PlayerInfo, left: PlayerInfo?,
- across: PlayerInfo?, right: PlayerInfo?,
- games: [GameType] = [], actions: [PlayerAction],
- cards: [PlayableCard],
- selectGame: Bool = false) {
- self.id = id
- self.name = name
- self.player = own
- self.playerLeft = left
- self.playerAcross = across
- self.playerRight = right
- self.playableGames = games.map { $0.id }
- self.actions = actions.map { $0.id }
- self.cards = cards.map { $0.cardInfo }
- self.playerSelectsGame = selectGame
+ init(table: AbstractTable, index: Int) {
+ self.id = table.id
+ self.name = table.name
+ self.player = table.playerInfo(forIndex: index)!
+ self.playerLeft = table.playerInfo(forIndex: (index + 1) % 4)
+ self.playerAcross = table.playerInfo(forIndex: (index + 2) % 4)
+ self.playerRight = table.playerInfo(forIndex: (index + 3) % 4)
+ let data = table.playerData(at: index)
+ self.playableGames = data.games.map { $0.id }
+ self.actions = data.actions.map { $0.id }
+ self.cards = data.cards.map { $0.cardInfo }
+ self.playerSelectsGame = data.selectsGame
+ self.game = table.playedGame?.id
}
}
diff --git a/Sources/App/Management/TableManagement.swift b/Sources/App/Management/TableManagement.swift
index a94af6b..627da99 100644
--- a/Sources/App/Management/TableManagement.swift
+++ b/Sources/App/Management/TableManagement.swift
@@ -10,7 +10,7 @@ typealias TableName = String
final class TableManagement: DiskWriter {
/// All tables indexed by their id
- private var tables = [TableId : Table]()
+ private var tables = [TableId : ManageableTable]()
/// The handle to the file where the tables are persisted
let storageFile: FileHandle
@@ -48,9 +48,7 @@ final class TableManagement: DiskWriter {
}
}
entries.forEach { id, tableData in
- let table = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic)
- tableData.players.forEach { _ = table.add(player: $0) }
- tables[id] = table
+ tables[id] = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic, players: tableData.players)
}
print("Loaded \(tables.count) tables")
}
@@ -63,7 +61,7 @@ final class TableManagement: DiskWriter {
- Returns: `true`, if the entry was written, `false` on error
*/
@discardableResult
- private func writeTableToDisk(table: Table) -> Bool {
+ private func writeTableToDisk(table: ManageableTable) -> Bool {
let visible = table.isPublic ? "public" : "private"
let players = table.playerNames
.joined(separator: ",")
@@ -94,8 +92,7 @@ final class TableManagement: DiskWriter {
- Returns: The table id
*/
func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo {
- let table = WaitingTable(newTable: name, isPublic: isPublic)
- _ = table.add(player: player)
+ let table = WaitingTable(newTable: name, isPublic: isPublic, creator: player)
tables[table.id] = table
writeTableToDisk(table: table)
return table.tableInfo(forPlayer: player)
@@ -115,8 +112,8 @@ final class TableManagement: DiskWriter {
currentTable(for: player)?.tableInfo(forPlayer: player)
}
- private func currentTable(for player: PlayerName) -> Table? {
- tables.values.first(where: { $0.contains(player: player) })
+ private func currentTable(for player: PlayerName) -> ManageableTable? {
+ tables.values.first(where: { $0.playerNames.contains(player) })
}
/**
@@ -154,6 +151,7 @@ final class TableManagement: DiskWriter {
guard let oldTable = currentTable(for: player) else {
return
}
+ /// `player.canStartGame` is automatically set to false, because table is not full
let table = WaitingTable(oldTable: oldTable, removing: player)
tables[table.id] = table
table.sendUpdateToAllPlayers()
diff --git a/Sources/App/Model/Card/Card+Array.swift b/Sources/App/Model/Card/Card+Array.swift
index 6991fce..3d1dd5f 100644
--- a/Sources/App/Model/Card/Card+Array.swift
+++ b/Sources/App/Model/Card/Card+Array.swift
@@ -6,6 +6,14 @@ typealias Hand = [Card]
extension Array where Element == Card {
+ var unplayable: [PlayableCard] {
+ map { $0.unplayable }
+ }
+
+ var playable: [PlayableCard] {
+ map { $0.playable }
+ }
+
var points: Int {
map { $0.points }
.reduce(0, +)
diff --git a/Sources/App/Model/Card/Card.swift b/Sources/App/Model/Card/Card.swift
index 146d6b8..93aeb65 100644
--- a/Sources/App/Model/Card/Card.swift
+++ b/Sources/App/Model/Card/Card.swift
@@ -10,6 +10,10 @@ struct Card: Codable {
case blatt = "B"
case herz = "H"
case schelln = "S"
+
+ var ace: Card {
+ .init(self, .ass)
+ }
}
let symbol: Symbol
@@ -45,6 +49,18 @@ struct Card: Codable {
var points: Int {
symbol.points
}
+
+ var playable: PlayableCard {
+ .init(card: self, isPlayable: true)
+ }
+
+ var unplayable: PlayableCard {
+ .init(card: self, isPlayable: false)
+ }
+
+ func playable(_ isPlayable: Bool) -> PlayableCard {
+ .init(card: self, isPlayable: isPlayable)
+ }
static let allCards: Set = {
let all = Card.Suit.allCases.map { suit in
diff --git a/Sources/App/Model/GameClass.swift b/Sources/App/Model/GameClass.swift
index fa45cd7..50e16d6 100644
--- a/Sources/App/Model/GameClass.swift
+++ b/Sources/App/Model/GameClass.swift
@@ -9,33 +9,65 @@ extension GameType {
case .bettel:
return .bettel
case .wenz, .geier:
- return .wenzGeier
+ return .wenz
case .soloEichel, .soloBlatt, .soloHerz, .soloSchelln:
return .solo
}
}
- enum GameClass: Int {
- case none = 0
- case ruf = 1
- case bettel = 2
- case wenzGeier = 3
- case solo = 4
+ enum GameClass: String {
+ case none = "none"
+ case ruf = "ruf"
+ case hochzeit = "hochzeit"
+ case bettel = "bettel"
+ case wenz = "wenz"
+ case geier = "geier"
+ case solo = "solo"
var cost: Int {
switch self {
case .none: return 0
case .ruf: return 5
+ case .hochzeit: return 10
case .bettel: return 15
- case .wenzGeier, .solo: return 20
+ case .wenz, .geier, .solo: return 20
}
}
- mutating func increase() {
- guard self != .solo else {
- return
+ @discardableResult
+ mutating func increase() -> Bool {
+ switch self {
+ case .none:
+ self = .ruf
+ case .ruf:
+ self = .bettel
+ case .hochzeit:
+ self = .bettel
+ case .bettel:
+ self = .wenz
+ case .wenz, .geier:
+ self = .solo
+ case .solo:
+ return false
+ }
+ return true
+ }
+
+ func increased() -> GameClass? {
+ switch self {
+ case .none:
+ return .ruf
+ case .ruf:
+ return .bettel
+ case .hochzeit:
+ return .bettel
+ case .bettel:
+ return .wenz
+ case .wenz, .geier:
+ return .solo
+ case .solo:
+ return nil
}
- self = .init(rawValue: rawValue + 1)!
}
var allowsWedding: Bool {
@@ -47,18 +79,39 @@ extension GameType {
}
}
+ func allows(game: GameType) -> Bool {
+ availableGames.contains(game)
+ }
+
var availableGames: [GameType] {
switch self {
case .none, .ruf:
return GameType.allCases
- case .bettel:
+ case .hochzeit, .bettel:
return [.bettel, .wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
- case .wenzGeier:
+ case .wenz, .geier:
return [.wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
case .solo:
return [.soloEichel, .soloBlatt, .soloHerz, .soloSchelln]
}
}
+
+ var availableClasses: [GameClass] {
+ switch self {
+ case .none, .ruf:
+ return [.ruf, .bettel, .wenz, .geier, .solo]
+ case .hochzeit, .bettel:
+ return [.bettel, .wenz, .geier, .solo]
+ case .wenz, .geier:
+ return [.wenz, .geier, .solo]
+ case .solo:
+ return [.solo]
+ }
+ }
+
+ var classesWhenOutbidding: [GameClass] {
+ increased()?.availableClasses ?? []
+ }
}
}
@@ -68,3 +121,10 @@ extension GameType.GameClass: Comparable {
lhs.rawValue < rhs.rawValue
}
}
+
+extension GameType.GameClass: GameConvertible {
+
+ var id: GameId {
+ rawValue
+ }
+}
diff --git a/Sources/App/Model/GameConvertible.swift b/Sources/App/Model/GameConvertible.swift
new file mode 100644
index 0000000..f7f156f
--- /dev/null
+++ b/Sources/App/Model/GameConvertible.swift
@@ -0,0 +1,6 @@
+import Foundation
+
+protocol GameConvertible {
+
+ var id: GameId { get }
+}
diff --git a/Sources/App/Model/GameType.swift b/Sources/App/Model/GameType.swift
index 42b9a86..ad96cdf 100644
--- a/Sources/App/Model/GameType.swift
+++ b/Sources/App/Model/GameType.swift
@@ -56,10 +56,6 @@ enum GameType: String, CaseIterable, Codable {
gameClass.cost
}
- var id: GameId {
- rawValue
- }
-
var sortingType: CardOrder.Type {
switch self {
case .wenz:
@@ -77,3 +73,11 @@ enum GameType: String, CaseIterable, Codable {
}
}
}
+
+extension GameType: GameConvertible {
+
+ var id: GameId {
+ rawValue
+ }
+
+}
diff --git a/Sources/App/Model/OldPlayer.swift b/Sources/App/Model/OldPlayer.swift
index f35732a..fcdaa45 100644
--- a/Sources/App/Model/OldPlayer.swift
+++ b/Sources/App/Model/OldPlayer.swift
@@ -304,6 +304,7 @@ final class OldPlayer {
actions = []
}
}
+
func didFinishGame() {
actions = [.deal]
}
diff --git a/Sources/App/Model/OldTable.swift b/Sources/App/Model/OldTable.swift
index 826ec9a..e098e42 100644
--- a/Sources/App/Model/OldTable.swift
+++ b/Sources/App/Model/OldTable.swift
@@ -380,7 +380,7 @@ final class OldTable {
// Remove wedding offers
players.forEach { $0.weddingOutbid() }
}
- #warning("Fix bidding")
+
// TODO: Remove highest bidder from old player
player.didPerformBid()
diff --git a/Sources/App/Model/PlayerState.swift b/Sources/App/Model/PlayerState.swift
new file mode 100644
index 0000000..f76c029
--- /dev/null
+++ b/Sources/App/Model/PlayerState.swift
@@ -0,0 +1,23 @@
+import Foundation
+
+enum PlayerState: String {
+
+ case canDouble
+ case didDouble
+
+ case isDisconnected
+
+ case mustBid
+ case didFold
+ case didBid
+ case mustPlaceBid
+
+ case isGameSelector
+ case isWeddingOfferer
+ case isCalled
+
+ case didRaise
+
+ case isWinner
+ case isLooser
+}
diff --git a/Sources/App/Model/Players/AbstractPlayer.swift b/Sources/App/Model/Players/AbstractPlayer.swift
deleted file mode 100644
index 94fda94..0000000
--- a/Sources/App/Model/Players/AbstractPlayer.swift
+++ /dev/null
@@ -1,26 +0,0 @@
-import Foundation
-import WebSocketKit
-
-class AbstractPlayer {
-
- let name: PlayerName
-
- var socket: WebSocket?
-
- init(name: PlayerName, socket: WebSocket? = nil) {
- self.name = name
- self.socket = socket
- }
-
- init(player: Player) {
- self.name = player.name
- self.socket = player.socket
- }
-}
-
-extension AbstractPlayer: Equatable {
-
- static func == (lhs: AbstractPlayer, rhs: AbstractPlayer) -> Bool {
- lhs.name == rhs.name
- }
-}
diff --git a/Sources/App/Model/Players/BiddingPlayer.swift b/Sources/App/Model/Players/BiddingPlayer.swift
index dba8c94..c84c277 100644
--- a/Sources/App/Model/Players/BiddingPlayer.swift
+++ b/Sources/App/Model/Players/BiddingPlayer.swift
@@ -1,51 +1,61 @@
import Foundation
import WebSocketKit
-final class BiddingPlayer {
-
- let name: String
-
- var socket: WebSocket?
-
- let cards: [PlayableCard]
+final class BiddingPlayer: Player {
var isStillBidding = true
- var isAllowedToOfferWedding = true
+ var isAllowedToOfferWedding: Bool
- var offersWedding = false
+ var selectsGame = false
- var wouldAcceptWedding = false
-
- init(player: DealingPlayer, cards: [PlayableCard]) {
- self.name = player.name
- self.socket = player.socket
- self.cards = cards
- }
-}
-
-extension BiddingPlayer: Player {
-
- var canOfferWedding: Bool {
- rawCards.canOfferWedding
+ init(player: DealingPlayer) {
+ isAllowedToOfferWedding = true
+ super.init(player: player)
}
- var rawCards: [Card] {
- cards.map { $0.card }
+ init(player: WeddingPlayer) {
+ isStillBidding = !player.offersWedding
+ isAllowedToOfferWedding = false
+ super.init(player: player)
}
+ func canPlay(game: GameType) -> Bool {
+ guard let suit = game.calledSuit else {
+ if game == .hochzeit {
+ return canOfferWedding
+ }
+ return true
+ }
+ let sorter = game.sortingType
+ 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)
+ }
- var actions: [PlayerAction] {
+ override var actions: [PlayerAction] {
guard isStillBidding else {
return []
}
- guard canOfferWedding, isAllowedToOfferWedding, !offersWedding else {
- return [.increaseOrMatchGame, .withdrawFromAuction]
+ var actions: [PlayerAction] = isNextActor ? [.increaseOrMatchGame, .withdrawFromAuction] : []
+ if canOfferWedding {
+ actions.append(.offerWedding)
}
- return [.increaseOrMatchGame, .withdrawFromAuction, .offerWedding]
+ return actions
}
- var playedCard: Card? {
- nil
+ override var points: Int? {
+ get { nil }
+ set { }
+ }
+}
+
+extension BiddingPlayer {
+
+ var canOfferWedding: Bool {
+ cards.canOfferWedding
}
}
diff --git a/Sources/App/Model/Players/DealingPlayer.swift b/Sources/App/Model/Players/DealingPlayer.swift
index fd3cac2..4f03220 100644
--- a/Sources/App/Model/Players/DealingPlayer.swift
+++ b/Sources/App/Model/Players/DealingPlayer.swift
@@ -1,24 +1,36 @@
import Foundation
import WebSocketKit
-final class DealingPlayer: AbstractPlayer {
-
- var cards: [PlayableCard] = []
+final class DealingPlayer: Player {
var didDouble: Bool? = nil
+ override var isNextActor: Bool {
+ get { didDouble == nil }
+ set { }
+ }
+
+ override var actions: [PlayerAction] {
+ didDouble == nil ? [.initialDoubleCost, .noDoubleCost] : []
+ }
+
init(player: WaitingPlayer) {
super.init(player: player)
}
-}
-extension DealingPlayer: Player {
-
- var actions: [PlayerAction] {
- didDouble == nil ? [.initialDoubleCost, .noDoubleCost] : []
+ override var numberOfDoubles: Int {
+ get { didDouble == true ? 1 : 0 }
+ set { }
}
- var playedCard: Card? {
- nil
+ override var leadsGame: Bool {
+ get { false }
+ set { }
}
+
+ override var points: Int? {
+ get { nil }
+ set { }
+ }
+
}
diff --git a/Sources/App/Model/Players/FinishedPlayer.swift b/Sources/App/Model/Players/FinishedPlayer.swift
new file mode 100644
index 0000000..15aac50
--- /dev/null
+++ b/Sources/App/Model/Players/FinishedPlayer.swift
@@ -0,0 +1,21 @@
+import Foundation
+
+final class FinishedPlayer: Player {
+
+ let tricks: [Trick]
+
+ init(player: PlayingPlayer) {
+ self.tricks = player.wonTricks
+ super.init(player: player)
+ }
+
+ override var points: Int? {
+ get { tricks.map { $0.points }.reduce(0, +) }
+ set { }
+ }
+
+ override var actions: [PlayerAction] {
+ [.deal]
+ }
+
+}
diff --git a/Sources/App/Model/Players/Player.swift b/Sources/App/Model/Players/Player.swift
index 79b7182..9db94b0 100644
--- a/Sources/App/Model/Players/Player.swift
+++ b/Sources/App/Model/Players/Player.swift
@@ -1,18 +1,57 @@
import Foundation
import WebSocketKit
-protocol Player: AnyObject {
+class Player {
- var name: String { get }
+ let name: PlayerName
- var socket: WebSocket? { get set }
+ var socket: WebSocket?
- var playedCard: Card? { get }
+ var playedCard: Card?
- var actions: [PlayerAction] { get }
+ var isNextActor: Bool
- var cards: [PlayableCard] { get }
+ var cards: [Card]
+ var numberOfDoubles: Int
+
+ var leadsGame: Bool
+
+ var points: Int?
+
+ init(name: PlayerName, socket: WebSocket? = nil) {
+ self.name = name
+ self.socket = socket
+ self.cards = []
+ self.isNextActor = false
+ self.playedCard = nil
+ self.numberOfDoubles = 0
+ self.leadsGame = false
+ self.points = nil
+ }
+
+ init(player: Player) {
+ self.name = player.name
+ self.socket = player.socket
+ self.cards = player.cards
+ self.isNextActor = false
+ self.playedCard = player.playedCard
+ self.numberOfDoubles = player.numberOfDoubles
+ self.leadsGame = player.leadsGame
+ self.points = player.points
+ }
+
+ var actions: [PlayerAction] {
+ []
+ }
+
+}
+
+extension Player: Equatable {
+
+ static func == (lhs: Player, rhs: Player) -> Bool {
+ lhs.name == rhs.name
+ }
}
extension Player {
@@ -36,6 +75,7 @@ extension Player {
self.socket = socket
}
+ @discardableResult
func disconnect() -> Bool {
guard let socket = socket else {
return false
@@ -50,8 +90,18 @@ extension Player {
}
- func send(_ info: TableInfo) {
- try? socket?.send(encodeJSON(info))
+ @discardableResult
+ func send(_ info: TableInfo) -> Bool {
+ guard let socket = socket else {
+ return false
+ }
+ do {
+ try socket.send(encodeJSON(info))
+ } catch {
+ print("Failed to send info: \(error)")
+ return false
+ }
+ return true
}
// MARK: Actions
@@ -61,3 +111,10 @@ extension Player {
}
}
+
+extension Player: CustomStringConvertible {
+
+ var description: String {
+ name
+ }
+}
diff --git a/Sources/App/Model/Players/PlayingPlayer.swift b/Sources/App/Model/Players/PlayingPlayer.swift
index 3b0088d..2a2af2a 100644
--- a/Sources/App/Model/Players/PlayingPlayer.swift
+++ b/Sources/App/Model/Players/PlayingPlayer.swift
@@ -1,30 +1,139 @@
import Foundation
import WebSocketKit
-final class PlayingPlayer: AbstractPlayer {
+/**
+ 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
- var playedCard: Card? = nil
-
- var cards: [PlayableCard]
-
- var leadsGame = false
+final class PlayingPlayer: Player {
var canStillRaise = true
- init(player: BiddingPlayer) {
- self.cards = player.cards
+ var isCalledWithAce: Card?
+
+ /// All tricks won by the player in this game
+ var wonTricks: [Trick] = []
+
+ init(player: Player, leads: Bool, calledAce ace: Card?) {
super.init(player: player)
+ leadsGame = leads
+ if let ace = ace, cards.contains(ace) {
+ isCalledWithAce = ace
+ } else {
+ isCalledWithAce = nil
+ }
}
-}
-extension PlayingPlayer: Player {
-
-
- var actions: [PlayerAction] {
- guard canStillRaise, !leadsGame else {
+ override var actions: [PlayerAction] {
+ guard canStillRaise, leadsGame == (isCalledWithAce != nil) else {
return []
}
return [.doubleDuringGame]
}
+ func play(card: Card) {
+ playedCard = card
+ cards = cards.filter { $0 != card }
+ if card == isCalledWithAce {
+ leadsGame.toggle()
+ isCalledWithAce = nil
+ }
+ }
+
+ func switchLead() {
+ leadsGame.toggle()
+ }
+
+ func sortCards(for game: GameType) {
+ cards = cards.sortedCards(forGame: game)
+ }
+
+ func canPlay(card: Card, for trick: Trick, in game: GameType) -> Bool {
+ playableCards(for: trick, in: game).contains { $0.card == card && $0.isPlayable }
+ }
+
+ func playableCards(for trick: Trick, in game: GameType) -> [PlayableCard] {
+ guard isNextActor else {
+ return cards.unplayable
+ }
+ guard cards.count > 1 else {
+ // Last card can always be played
+ return cards.playable
+ }
+ guard let firstCard = trick.first else {
+ return playableCardsForStarter(game: game)
+ }
+
+ let sorter = game.sortingType
+
+ guard sorter.isTrump(firstCard) else {
+ return playableCardsFollowing(suit: firstCard.suit, game: game)
+ }
+ guard !sorter.hasTrump(in: cards) else {
+ // Must follow with trump
+ return cards.map { $0.playable(sorter.isTrump($0)) }
+ }
+ // Can play any card if not in calling game
+ guard let suit = game.calledSuit else {
+ return cards.playable
+ }
+ // Can play any card, except the called ace
+ let ace = Card(suit, .ass)
+ return cards.map { $0.playable($0 != ace) }
+ }
+
+ private func playableCardsFollowing(suit playedSuit: Card.Suit, game: GameType) -> [PlayableCard] {
+ let sorter = game.sortingType
+ let suitCards = sorter.cards(with: playedSuit, in: cards)
+
+ func followSuit() -> [PlayableCard] {
+ cards.map { $0.playable(!sorter.isTrump($0) && $0.suit == playedSuit) }
+ }
+
+ guard let calledSuit = game.calledSuit else {
+ return suitCards.isEmpty ? cards.playable : followSuit()
+ }
+ let ace = Card(calledSuit, .ass)
+ guard !suitCards.isEmpty else {
+ // Exclude called ace, all others allowed
+ return cards.map { $0.playable($0 != ace) }
+ }
+ guard calledSuit == playedSuit else {
+ // Must follow suit (called ace not present)
+ return followSuit()
+ }
+
+ // The called suit is played, must commit ace
+ guard cards.contains(ace) else {
+ // Must follow suit
+ return followSuit()
+ }
+ // Must play ace
+ return cards.map { $0.playable($0 == ace) }
+ }
+
+ private func playableCardsForStarter(game: GameType) -> [PlayableCard] {
+ guard let suit = game.calledSuit else {
+ return cards.playable
+ }
+ let ace = Card(suit, .ass)
+ // Check if called ace exists, to prohibit other cards of the same suit
+ guard cards.contains(ace) else {
+ return cards.playable
+ }
+ // Jodeln
+ if cards.count == numberOfCardsPerPlayer,
+ cards.suitCount(suit, in: game) >= numberOfCardsToProtectAce {
+ return cards.playable
+ }
+
+ // Only ace allowed for the called suit
+ return cards.map { $0.playable($0.suit != suit || $0.symbol.isTrumpOrAce) }
+ }
+
+ var currentPoints: Int {
+ wonTricks.map { $0.points }.reduce(0, +)
+ }
}
diff --git a/Sources/App/Model/Players/WaitingPlayer.swift b/Sources/App/Model/Players/WaitingPlayer.swift
index 19ac291..25b2f25 100644
--- a/Sources/App/Model/Players/WaitingPlayer.swift
+++ b/Sources/App/Model/Players/WaitingPlayer.swift
@@ -1,22 +1,22 @@
import Foundation
import WebSocketKit
-final class WaitingPlayer: AbstractPlayer {
+final class WaitingPlayer: Player {
var canStartGame: Bool = false
-}
-extension WaitingPlayer: Player {
-
- var actions: [PlayerAction] {
+ override var actions: [PlayerAction] {
canStartGame ? [.deal] : []
}
- var cards: [PlayableCard] {
- []
+ override var leadsGame: Bool {
+ get { false }
+ set { }
}
- var playedCard: Card? {
- nil
+ override var points: Int? {
+ get { nil }
+ set { }
}
+
}
diff --git a/Sources/App/Model/Players/WeddingPlayer.swift b/Sources/App/Model/Players/WeddingPlayer.swift
new file mode 100644
index 0000000..7ce78f5
--- /dev/null
+++ b/Sources/App/Model/Players/WeddingPlayer.swift
@@ -0,0 +1,89 @@
+import Foundation
+
+final class WeddingPlayer: Player {
+
+ enum State {
+ case requiresAction
+ case offersWedding
+ case wouldAcceptWedding
+ case withdrawnFromAuction
+ case selectsGame
+ }
+
+ var state: State
+
+ var requiresAction: Bool {
+ state == .requiresAction
+ }
+
+ var selectsGame: Bool {
+ get {
+ state == .selectsGame
+ }
+ set {
+ state = .selectsGame
+ }
+ }
+
+ var wouldAcceptWedding: Bool {
+ state == .wouldAcceptWedding
+ }
+
+ var offersWedding: Bool {
+ state == .offersWedding
+ }
+
+ init(player: BiddingPlayer, offersWedding: Bool) {
+ self.state = offersWedding ? .offersWedding : .requiresAction
+ super.init(player: player)
+ }
+
+ override var actions: [PlayerAction] {
+ guard state == .requiresAction else {
+ return []
+ }
+ return [.increaseOrMatchGame, .withdrawFromAuction, .acceptWedding]
+ }
+
+ override var isNextActor: Bool {
+ get {
+ switch state {
+ case .requiresAction, .selectsGame:
+ return true
+ default:
+ return false
+ }
+ }
+ set { }
+ }
+
+ override var points: Int? {
+ get { nil }
+ set { }
+ }
+
+ override var leadsGame: Bool {
+ get { offersWedding || selectsGame }
+ set { }
+ }
+
+ func canExchange(card: Card) -> Bool {
+ cards.filter { !$0.isTrump(in: .hochzeit) }.contains(card)
+ }
+
+ var exchangeableCards: [PlayableCard] {
+ cards.map { $0.playable(!$0.isTrump(in: .hochzeit)) }
+ }
+
+ func replaceWeddingCard(with card: Card) -> Card {
+ let ownCardIndex = cards.firstIndex { $0.isTrump(in: .hochzeit)}!
+ let ownCard = cards.remove(at: ownCardIndex)
+ cards.append(card)
+ cards = cards.sortedCards(forGame: .hochzeit)
+ return ownCard
+ }
+
+ func replace(_ card: Card, with trumpCard: Card) {
+ cards = (cards.filter { $0 != card } + [trumpCard]).sortedCards(forGame: .hochzeit)
+ }
+}
diff --git a/Sources/App/Model/Tables/AbstractTable.swift b/Sources/App/Model/Tables/AbstractTable.swift
index d9ab4c8..1ceef88 100644
--- a/Sources/App/Model/Tables/AbstractTable.swift
+++ b/Sources/App/Model/Tables/AbstractTable.swift
@@ -1,6 +1,7 @@
import Foundation
+import WebSocketKit
-class AbstractTable {
+class AbstractTable where TablePlayer: Player {
/// The unique id of the table
let id: TableId
@@ -11,15 +12,116 @@ class AbstractTable {
/// Indicates that the table is visible to all players, and can be joined by anyone
let isPublic: Bool
- init(table: AbstractTable) {
+ /**
+ The players sitting at the table.
+
+ The players are ordered clockwise around the table, with the first player starting the game.
+ */
+ var players: [TablePlayer]
+
+ var playedGame: GameType? {
+ nil
+ }
+
+ init(table: ManageableTable, players: [TablePlayer]) {
self.id = table.id
self.name = table.name
self.isPublic = table.isPublic
+ self.players = players
}
- init(id: TableId, name: TableName, isPublic: Bool) {
+ init(id: TableId, name: TableName, isPublic: Bool, players: [TablePlayer]) {
self.id = id
self.name = name
self.isPublic = isPublic
+ self.players = players
}
+
+ func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ (.tableStateInvalid, nil)
+ }
+
+ func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ (.tableStateInvalid, nil)
+ }
+
+ func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
+ let player = players[index]
+ return (actions: player.actions, games: [], cards: player.cards.unplayable, selectsGame: false)
+ }
+
+ func cardStackPosition(ofPlayerAt index: Int) -> Int {
+ index
+ }
+
}
+
+extension AbstractTable: ManageableTable {
+
+ var publicInfo: PublicTableInfo {
+ .init(id: id, name: name, players: playerNames)
+ }
+
+ var playerNames: [PlayerName] {
+ players.map { $0.name }
+ }
+
+ var allPlayers: [Player] {
+ players
+ }
+
+
+
+ // MARK: Connection
+
+ func connect(player name: PlayerName, using socket: WebSocket) -> Bool {
+ guard let player = players.player(named: name) else {
+ return false
+ }
+ player.connect(using: socket)
+ sendUpdateToAllPlayers()
+ return true
+ }
+
+ func disconnect(player name: PlayerName) {
+ guard let player = players.player(named: name) else {
+ return
+ }
+ guard player.disconnect() else {
+ return
+ }
+ sendUpdateToAllPlayers()
+ return
+ }
+
+ func sendUpdateToAllPlayers() {
+ players.enumerated().forEach { playerIndex, player in
+ guard player.isConnected else {
+ return
+ }
+ let info = self.tableInfo(forPlayerAt: playerIndex)
+ player.send(info)
+ }
+ }
+
+ // MARK: Client info
+
+ func playerInfo(forIndex index: Int) -> PlayerInfo? {
+ guard let player = players.at(index) else {
+ return nil
+ }
+ let height = cardStackPosition(ofPlayerAt: index)
+ return PlayerInfo(player: player, position: height)
+ }
+
+ func tableInfo(forPlayer player: PlayerName) -> TableInfo {
+ let index = players.index(of: player)
+ return tableInfo(forPlayerAt: index)
+ }
+
+ func tableInfo(forPlayerAt index: Int) -> TableInfo {
+ .init(table: self, index: index)
+ }
+
+}
+
diff --git a/Sources/App/Model/Tables/BiddingTable.swift b/Sources/App/Model/Tables/BiddingTable.swift
index 3ab5c74..b9e5b62 100644
--- a/Sources/App/Model/Tables/BiddingTable.swift
+++ b/Sources/App/Model/Tables/BiddingTable.swift
@@ -1,51 +1,181 @@
import Foundation
-final class BiddingTable: AbstractTable {
+final class BiddingTable: AbstractTable {
+
+ var gameToOutbid: GameType.GameClass = .none
- var players: [BiddingPlayer]
+ var indexOfHighestBidder = 0
- var hasSelectedGame: Bool {
- // TODO: Implement
- false
+ var remainingBidders: Int {
+ players.filter { $0.isStillBidding }.count
+ }
+
+ var isWaitingForGameSelection: Bool {
+ players.contains { $0.selectsGame }
}
init(table: DealingTable) {
- self.players = table.players.map {
- BiddingPlayer(player: $0, cards: [])
+ // Add new cards to the players
+ let newCards = Dealer.dealRemainingCards(of: table.players.map { $0.cards })
+ let players: [BiddingPlayer] = table.players.enumerated().map { index, player in
+ player.cards = (player.cards + newCards[index])
+ .sortedCards(order: NormalCardOrder.self)
+ player.isNextActor = false
+ return BiddingPlayer(player: player)
}
- super.init(table: table)
+ players.first!.isNextActor = true
+ super.init(table: table, players: players)
}
- func select(game: GameType, player: PlayerName) -> (result: PlayerActionResult, table: Table?) {
- // TODO: Implement
- return (.tableStateInvalid, nil)
+ init(wedding table: WeddingTable, outbidBy player: WeddingPlayer) {
+ gameToOutbid = .hochzeit
+ indexOfHighestBidder = table.players.index(of: player)
+ // All players can bid again, except the wedding offerer
+ let players = table.players.map(BiddingPlayer.init)
+ players[indexOfHighestBidder].isNextActor = true
+ super.init(table: table, players: players)
+
+ // Choose the player after the one who discarded the wedding
+ selectNextBidder()
}
- func makePlayingTable() -> PlayingTable {
- // TODO: Implement
- fatalError()
- }
-}
+ func select(game: GameType, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard let player = players.player(named: name) else {
+ print("Player \(name) unexpectedly missing from bidding table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.selectsGame else {
+ print("Player \(name) does not select the game")
+ return (.tableStateInvalid, nil)
+ }
+ guard gameToOutbid.allows(game: game) else {
+ print("Game \(game) not allowed for class \(gameToOutbid)")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.canPlay(game: game) else {
+ print("Player \(game) can't play game \(game)")
+ return (.tableStateInvalid, nil)
+ }
-extension BiddingTable: Table {
-
- var allPlayers: [Player] {
- players
+ let table = PlayingTable(table: self, game: game, playedBy: player)
+ return (.success, table)
}
- var indexOfNextActor: Int {
- // TODO: Implement
- return 0
+ @discardableResult
+ private func selectNextBidder() -> Bool {
+ guard let index = players.firstIndex(where: { $0.isNextActor }) else {
+ print("Bidding: No current actor found to select next bidder")
+ return false
+ }
+ players[index].isNextActor = false
+ let newActor = players.rotated(toStartAt: (index + 1) % 4).first(where: { $0.isStillBidding })!
+ newActor.isNextActor = true
+ return true
}
- func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) {
- // TODO: Implement bidding actions
- return (.tableStateInvalid, nil)
+ override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard let player = players.player(named: name) else {
+ print("Player \(name) unexpectedly missing from bidding table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.canPerform(action) else {
+ return (.tableStateInvalid, nil)
+ }
+
+ switch action {
+ case .offerWedding:
+ return performWeddingOffer(forPlayer: player)
+ case .increaseOrMatchGame:
+ return performBidIncrease(forPlayer: player)
+ case .withdrawFromAuction:
+ return performWithdrawl(forPlayer: player)
+ default:
+ return (.tableStateInvalid, nil)
+ }
}
- func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
- // TODO: Implement for wedding
- return (.tableStateInvalid, nil)
+ private func performWeddingOffer(forPlayer player: BiddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard gameToOutbid.allowsWedding else {
+ return (.tableStateInvalid, nil)
+ }
+ let newTable = WeddingTable(table: self, offerer: player)
+ return (.success, newTable)
+ }
+
+ private func performBidIncrease(forPlayer player: BiddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard player.isNextActor, player.isStillBidding else {
+ return (.tableStateInvalid, nil)
+ }
+ let index = players.index(of: player)
+ if index < indexOfHighestBidder {
+ // Player sits before the current highest bidder, so only needs to match the game
+ indexOfHighestBidder = index
+ if gameToOutbid == .solo {
+ // Can't be outbid, so player selects game
+ players.forEach { $0.isStillBidding = false }
+ player.selectsGame = true
+ return (.success, nil)
+ }
+ // TODO: Check that wedding can be offered at the correct times
+ // There may be a case where a player sitting before the highest bidder
+ // can't offer a wedding anymore although it should be able to
+ if !gameToOutbid.allowsWedding {
+ players.forEach { $0.isAllowedToOfferWedding = false }
+ }
+ } else {
+ // Player sits after the highest bidder, so must outbid the game
+ // Also the case when first starting bidding
+ gameToOutbid.increase()
+ indexOfHighestBidder = index
+ if !gameToOutbid.allowsWedding {
+ players.forEach { $0.isAllowedToOfferWedding = false }
+ }
+ }
+ selectNextBidder()
+ return (.success, nil)
+ }
+
+ private func performWithdrawl(forPlayer player: BiddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard player.isStillBidding else {
+ return (.tableStateInvalid, nil)
+ }
+ player.isStillBidding = false
+ switch remainingBidders {
+ case 0:
+ // Nobody wants to play something, so abort the game
+ // This case can only be reached when nobody has bid yet
+ let table = WaitingTable(oldTableAdvancedByOne: self)
+ return (.success, table)
+ case 1:
+ if gameToOutbid != .none {
+ // Last player must play
+ player.isNextActor = false
+ indexOfHighestBidder = players.firstIndex { $0.isStillBidding == true }!
+ let highestPlayer = players[indexOfHighestBidder]
+ highestPlayer.isStillBidding = false
+ highestPlayer.selectsGame = true
+ highestPlayer.isNextActor = true
+ return (.success, nil)
+ }
+ default:
+ break
+ }
+ selectNextBidder()
+ return (.success, nil)
+ }
+
+ override func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
+ let player = players[index]
+
+ let games: [GameConvertible]
+ if isWaitingForGameSelection {
+ games = gameToOutbid.availableGames.filter(player.canPlay)
+ } else if index <= indexOfHighestBidder {
+ games = gameToOutbid.availableClasses
+ } else {
+ games = gameToOutbid.classesWhenOutbidding
+ }
+ return (player.actions, games, player.cards.unplayable, selectsGame: player.selectsGame)
}
}
diff --git a/Sources/App/Model/Tables/DealingTable.swift b/Sources/App/Model/Tables/DealingTable.swift
index 219e8bb..80c516d 100644
--- a/Sources/App/Model/Tables/DealingTable.swift
+++ b/Sources/App/Model/Tables/DealingTable.swift
@@ -1,39 +1,61 @@
import Foundation
-final class DealingTable: AbstractTable {
-
- var players: [DealingPlayer]
+final class DealingTable: AbstractTable {
init(table: WaitingTable) {
- self.players = table.players.map(DealingPlayer.init)
- super.init(table: table)
-
let cards = Dealer.dealFirstCards()
- for (index, player) in players.enumerated() {
- player.cards = cards[index].map { .init(card: $0, isPlayable: false) }
+ for (index, player) in table.players.enumerated() {
+ player.cards = cards[index]
+ }
+ let players = table.players.map(DealingPlayer.init)
+ super.init(table: table, players: players)
+ }
+
+ /// All players either doubled or didn't double
+ var allPlayersActed: Bool {
+ !players.contains { $0.didDouble == nil }
+ }
+
+ /// At least one player placed a bid
+ var hasDouble: Bool {
+ players.contains { $0.didDouble == true }
+ }
+
+ override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard let player = players.player(named: name) else {
+ print("Player \(name) unexpectedly missing from dealing table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.canPerform(action) else {
+ return (.tableStateInvalid, nil)
+ }
+
+ switch action {
+ case .initialDoubleCost:
+ return perform(double: true, forPlayer: player)
+ case .noDoubleCost:
+ return perform(double: false, forPlayer: player)
+ default:
+ return (.tableStateInvalid, nil)
}
}
-}
-
-extension DealingTable: Table {
-
- var allPlayers: [Player] {
- players
- }
-
- var indexOfNextActor: Int {
- // TODO: Implement
- return 0
- }
-
- func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) {
- // TODO: Implement doubling, additional cards
- return (.tableStateInvalid, nil)
- }
-
- func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
- // No cards playable while dealing
- (.tableStateInvalid, nil)
- }
-
+
+ private func perform(double: Bool, forPlayer player: DealingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard player.didDouble == nil else {
+ return (.tableStateInvalid, nil)
+ }
+ player.didDouble = double
+ guard allPlayersActed else {
+ return (.success, nil)
+ }
+ guard hasDouble else {
+ // Revert to a waiting table and switch to the next player
+ let table = WaitingTable(oldTableAdvancedByOne: self)
+ return (.success, table)
+ }
+ // Automatically adds remaining cards to the players
+ let table = BiddingTable(table: self)
+ return (.success, table)
+ }
+
}
diff --git a/Sources/App/Model/Tables/FinishedTable.swift b/Sources/App/Model/Tables/FinishedTable.swift
new file mode 100644
index 0000000..2ba35eb
--- /dev/null
+++ b/Sources/App/Model/Tables/FinishedTable.swift
@@ -0,0 +1,83 @@
+import Foundation
+
+final class FinishedTable: AbstractTable {
+
+ let game: GameType
+
+ var winners: [FinishedPlayer] {
+ leadersHaveWon ? leaders : opponents
+ }
+
+ var loosers: [FinishedPlayer] {
+ leadersHaveWon ? opponents : leaders
+ }
+
+ var leaders: [FinishedPlayer] {
+ players.filter { $0.leadsGame }
+ }
+
+ var opponents: [FinishedPlayer] {
+ players.filter { !$0.leadsGame }
+ }
+
+ var winningPoints: Int {
+ leadersHaveWon ? leadingPoints : 120 - leadingPoints
+ }
+
+ var loosingPoints: Int {
+ leadersHaveWon ? 120 - leadingPoints : leadingPoints
+ }
+
+ let leadingPoints: Int
+
+ var leadersHaveWon: Bool {
+ leadingPoints > 60
+ }
+
+ var isSchwarz: Bool {
+ loosingPoints == 0
+ }
+
+ var isSchneider: Bool {
+ loosingPoints < (leadersHaveWon ? 30 : 31)
+ }
+
+ override var playedGame: GameType? {
+ game
+ }
+
+ init(table: PlayingTable) {
+ let players = table.players.map(FinishedPlayer.init)
+ self.game = table.game
+ leadingPoints = players
+ .filter { $0.leadsGame }
+ .map { $0.points! }
+ .reduce(0, +)
+ super.init(table: table, players: players)
+ }
+
+ /**
+ Perform a deal action on the finished table.
+
+ - Parameter action: The action to perform
+ - Parameter player: The name of the player
+ */
+ override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ // Only dealing is allowed...
+ guard action == .deal else {
+ return (.tableStateInvalid, nil)
+ }
+ guard let player = players.player(named: name) else {
+ print("Unexpectedly missing player \(name) for deal action at finished table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+
+ guard player.canPerform(.deal) else {
+ print("Finished table: Player \(name) can't perform deal")
+ return (.tableStateInvalid, nil)
+ }
+ let waiting = WaitingTable(oldTableAdvancedByOne: self)
+ let table = DealingTable(table: waiting)
+ return (.success, table)
+ }
+}
diff --git a/Sources/App/Model/Tables/ManageableTable.swift b/Sources/App/Model/Tables/ManageableTable.swift
new file mode 100644
index 0000000..f3656ce
--- /dev/null
+++ b/Sources/App/Model/Tables/ManageableTable.swift
@@ -0,0 +1,32 @@
+import Foundation
+import WebSocketKit
+
+protocol ManageableTable {
+
+ /// The unique id of the table
+ var id: TableId { get }
+
+ /// The name of the table
+ var name: TableName { get }
+
+ /// The table is visible in the list of tables and can be joined by anyone
+ var isPublic: Bool { get }
+
+ var playerNames: [PlayerName] { get }
+
+ var allPlayers: [Player] { get }
+
+ var publicInfo: PublicTableInfo { get }
+
+ func tableInfo(forPlayer player: PlayerName) -> TableInfo
+
+ func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?)
+
+ func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?)
+
+ func connect(player name: PlayerName, using socket: WebSocket) -> Bool
+
+ func disconnect(player name: PlayerName)
+
+ func sendUpdateToAllPlayers()
+}
diff --git a/Sources/App/Model/Tables/PlayingTable.swift b/Sources/App/Model/Tables/PlayingTable.swift
index 04ee339..7764c02 100644
--- a/Sources/App/Model/Tables/PlayingTable.swift
+++ b/Sources/App/Model/Tables/PlayingTable.swift
@@ -1,34 +1,144 @@
import Foundation
-final class PlayingTable: AbstractTable {
+final class PlayingTable: AbstractTable {
- var players: [PlayingPlayer]
+ let game: GameType
- init(table: BiddingTable) {
- self.players = table.players.map(PlayingPlayer.init)
- super.init(table: table)
+ var indexOfTrickStarter = 0
+
+ var didDoubleInCurrentRound = false
+
+ var hasCompletedTrick: Bool {
+ !players.contains { $0.playedCard == nil }
+ }
+
+ var nextTrick: [Card] {
+ hasCompletedTrick ? [] : currentTrick
+ }
+
+ var currentTrick: [Card] {
+ players.rotated(toStartAt: indexOfTrickStarter).compactMap { $0.playedCard }
+ }
+
+ var completedTrick: Trick? {
+ let trick = currentTrick
+ guard trick.count == maximumPlayersPerTable else {
+ return nil
+ }
+ return trick
+ }
+
+ var allCardsPlayed: Bool {
+ !players.contains { !$0.cards.isEmpty }
+ }
+
+ override var playedGame: GameType? {
+ game
+ }
+
+ convenience init(table: BiddingTable, game: GameType, playedBy player: BiddingPlayer) {
+ let calledAce = game.calledSuit?.ace
+ let players = table.players.map {
+ PlayingPlayer(player: $0, leads: $0 == player, calledAce: calledAce)
+ }
+ self.init(table: table, players: players, game: game)
+ }
+
+ convenience init(wedding table: WeddingTable, offeredBy offerer: WeddingPlayer, acceptedBy player: WeddingPlayer) {
+ let players = table.players.map {
+ PlayingPlayer(player: $0, leads: $0 == player || $0 == offerer, calledAce: nil)
+ }
+ self.init(table: table, players: players, game: .hochzeit)
+ }
+
+ private init(table: ManageableTable, players: [PlayingPlayer], game: GameType) {
+ self.game = game
+ super.init(table: table, players: players)
+ players.forEach { $0.sortCards(for: game) }
+ players.first!.isNextActor = true
+ }
+
+ override func cardStackPosition(ofPlayerAt index: Int) -> Int {
+ (4 + index - indexOfTrickStarter) % 4
+ }
+
+ override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard let player = players.player(named: name) else {
+ print("Player \(name) unexpectedly missing from playing table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+ guard action == .doubleDuringGame else {
+ print("Player \(name) wants to perform action \(action) on playing table")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.canPerform(.doubleDuringGame) else {
+ print("Player \(name) is not allowed to raise")
+ return (.tableStateInvalid, nil)
+ }
+ player.numberOfDoubles += 1
+ players.forEach { $0.switchLead() }
+ self.didDoubleInCurrentRound = true
+ return (.success, nil)
+ }
+
+ override func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard let player = players.player(named: name) else {
+ print("Player \(name) unexpectedly missing from playing table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.isNextActor else {
+ print("Player \(name) wants to play card but is not active")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.canPlay(card: card, for: nextTrick, in: game) else {
+ return (.tableStateInvalid, nil)
+ }
+ if hasCompletedTrick {
+ players.forEach { $0.playedCard = nil }
+ indexOfTrickStarter = players.index(of: player)
+ }
+ player.play(card: card)
+ if let completedTrick = completedTrick {
+ return didFinish(trick: completedTrick, in: game)
+ } else {
+ let next = players.next(after: player)
+ next.isNextActor = true
+ player.isNextActor = false
+ return (.success, nil)
+ }
+ }
+
+ override func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
+ let player = players[index]
+ let cards = player.playableCards(for: nextTrick, in: game)
+ return (actions: player.actions, games: [], cards: cards, selectsGame: false)
+ }
+
+ private func didFinish(trick: Trick, in game: GameType) -> (result: PlayerActionResult, table: ManageableTable?) {
+ let index = trick.highCardIndex(forGame: game)
+ let winner = players[(indexOfTrickStarter + index) % 4]
+ players.forEach {
+ $0.isNextActor = false
+ $0.canStillRaise = didDoubleInCurrentRound
+ }
+ winner.wonTricks.append(trick)
+ winner.isNextActor = true
+
+ if game == .bettel && winner.leadsGame {
+ // A bettel is lost if a single trick is won by the leader
+ return finishedGame()
+ }
+
+ didDoubleInCurrentRound = false
+ if allCardsPlayed {
+ return finishedGame()
+ }
+ return (.success, nil)
+ }
+
+ private func finishedGame() -> (result: PlayerActionResult, table: ManageableTable?) {
+ let table = FinishedTable(table: self)
+ print("\(table.winners) have won with \(table.winningPoints) to \(table.loosingPoints) points")
+ return (.success, table)
}
}
-
-extension PlayingTable: Table {
-
- var allPlayers: [Player] {
- players
- }
-
- var indexOfNextActor: Int {
- // TODO: Implement
- return 0
- }
-
- func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) {
- // TODO: Implement raises
- return (.tableStateInvalid, nil)
- }
-
- func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
- // TODO: Implement playing of cards
- return (.tableStateInvalid, nil)
- }
-
-}
diff --git a/Sources/App/Model/Tables/Table.swift b/Sources/App/Model/Tables/Table.swift
deleted file mode 100644
index f592829..0000000
--- a/Sources/App/Model/Tables/Table.swift
+++ /dev/null
@@ -1,121 +0,0 @@
-import Foundation
-import WebSocketKit
-
-protocol Table: AbstractTable {
-
- /// The unique id of the table
- var id: TableId { get }
-
- /// The name of the table
- var name: TableName { get }
-
- /// The table is visible in the list of tables and can be joined by anyone
- var isPublic: Bool { get }
-
- /**
- The players sitting at the table.
-
- The players are ordered clockwise around the table, with the first player starting the game.
- */
- var allPlayers: [Player] { get }
-
- var indexOfNextActor: Int { get }
-
- func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?)
-
- func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?)
-}
-
-extension Table {
-
- var playerNames: [String] {
- allPlayers.map { $0.name }
- }
-
-
- func index(of player: PlayerName) -> Int {
- allPlayers.firstIndex { $0.name == player }!
- }
-
- func player(named name: PlayerName) -> Player? {
- allPlayers.first { $0.name == name }
- }
-
- func contains(player: PlayerName) -> Bool {
- allPlayers.contains { $0.name == player }
- }
-
- // MARK: Connection
-
- func sendUpdateToAllPlayers() {
- allPlayers.enumerated().forEach { playerIndex, player in
- guard player.isConnected else {
- return
- }
- let info = self.tableInfo(forPlayerAt: playerIndex)
- player.send(info)
- }
- }
-
- func connect(player name: PlayerName, using socket: WebSocket) -> Bool {
- guard let player = player(named: name) else {
- return false
- }
- player.connect(using: socket)
- sendUpdateToAllPlayers()
- return true
- }
-
- func disconnect(player name: PlayerName) {
- guard let player = player(named: name) else {
- return
- }
- guard player.disconnect() else {
- return
- }
- sendUpdateToAllPlayers()
- return
- }
-
- // MARK: Client info
-
- var publicInfo: PublicTableInfo {
- .init(id: id, name: name, players: playerNames)
- }
-
- private func player(forIndex index: Int) -> Player? {
- let players = allPlayers
- guard index < players.count else {
- return nil
- }
- return players[index]
- }
-
- private func playerInfo(forIndex index: Int) -> PlayerInfo? {
- guard let player = player(forIndex: index) else {
- return nil
- }
- let isNext = indexOfNextActor == index
- return PlayerInfo(player: player, isNextActor: isNext, position: index)
- }
-
- func tableInfo(forPlayer player: PlayerName) -> TableInfo {
- let index = index(of: player)
- return tableInfo(forPlayerAt: index)
- }
-
- func tableInfo(forPlayerAt index: Int) -> TableInfo {
- let player = player(forIndex: index)!
- let own = playerInfo(forIndex: index)!
- let left = playerInfo(forIndex: (index + 1) % 4)
- let across = playerInfo(forIndex: (index + 2) % 4)
- let right = playerInfo(forIndex: (index + 3) % 4)
- return .init(
- id: id, name: name,
- own: own, left: left,
- across: across, right: right,
- actions: player.actions,
- cards: player.cards)
- }
-
-}
diff --git a/Sources/App/Model/Tables/WaitingTable.swift b/Sources/App/Model/Tables/WaitingTable.swift
index 4ba9bc3..04228ad 100644
--- a/Sources/App/Model/Tables/WaitingTable.swift
+++ b/Sources/App/Model/Tables/WaitingTable.swift
@@ -3,22 +3,20 @@ import Foundation
/**
Represents a table where players are still joining and leaving.
*/
-final class WaitingTable: AbstractTable {
-
- /**
- The players sitting at the table.
-
- The players are ordered clockwise around the table, with the first player starting the game.
- */
- var players: [WaitingPlayer] = []
+final class WaitingTable: AbstractTable {
/// The table contains enough players to start a game
var isFull: Bool {
players.count >= maximumPlayersPerTable
}
- override init(id: TableId, name: TableName, isPublic: Bool) {
- super.init(id: id, name: name, isPublic: isPublic)
+ init(id: TableId, name: TableName, isPublic: Bool, players: [PlayerName]) {
+ let players = players.map { WaitingPlayer(name: $0) }
+ players.first!.isNextActor = true
+ super.init(id: id, name: name, isPublic: isPublic, players: players)
+ if isFull {
+ self.players.forEach { $0.canStartGame = true }
+ }
}
/**
@@ -26,8 +24,10 @@ final class WaitingTable: AbstractTable {
- Parameter name: The name of the table
- Parameter isPublic: The table is visible and joinable by everyone
*/
- init(newTable name: TableName, isPublic: Bool) {
- super.init(id: .newToken(), name: name, isPublic: isPublic)
+ init(newTable name: TableName, isPublic: Bool, creator: PlayerName) {
+ let player = WaitingPlayer(name: creator)
+ player.isNextActor = true
+ super.init(id: .newToken(), name: name, isPublic: isPublic, players: [player])
}
/**
@@ -35,13 +35,35 @@ final class WaitingTable: AbstractTable {
This is needed when a player leaves an active table.
- Parameter oldTable: The table to convert
- - Parameter player: The player to remove from the table.
+ - Parameter player: The name of the player to remove from the table.
*/
- init(oldTable: Table, removing player: PlayerName) {
- self.players = oldTable.allPlayers
- .filter { $0.name != player }
- .map(WaitingPlayer.init)
- super.init(table: oldTable)
+ init(oldTable: ManageableTable, removing player: PlayerName) {
+ let players = oldTable.allPlayers
+ .filter {
+ guard $0.name == player else {
+ return true
+ }
+ _ = $0.disconnect()
+ return false
+ }
+ .map { WaitingPlayer(name: $0.name, socket: $0.socket) }
+ players.first!.isNextActor = true
+ super.init(table: oldTable, players: players)
+ }
+
+ /**
+ Convert another table to a waiting table.
+
+ This is needed when a player leaves an active table.
+ - Parameter oldTable: The table to convert
+ */
+ init(oldTableAdvancedByOne table: ManageableTable) {
+ let players = table.allPlayers
+ .rotatedByOne()
+ .map { WaitingPlayer(name: $0.name, socket: $0.socket) }
+ super.init(table: table, players: players)
+ players.forEach { $0.canStartGame = true }
+ players.first!.isNextActor = true
}
/**
@@ -69,7 +91,7 @@ final class WaitingTable: AbstractTable {
- Parameter action: The action to perform
- Parameter player: The name of the player
*/
- func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
+ override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
// Only dealing is allowed...
guard action == .deal else {
return (.tableStateInvalid, nil)
@@ -78,7 +100,7 @@ final class WaitingTable: AbstractTable {
guard isFull else {
return (.tableStateInvalid, nil)
}
- guard let player = player(named: name) else {
+ guard let player = players.player(named: name) else {
print("Unexpected action \(action) for missing player \(name) at table \(self.name)")
return (.tableStateInvalid, nil)
}
@@ -90,20 +112,8 @@ final class WaitingTable: AbstractTable {
let table = DealingTable(table: self)
return (.success, table)
}
-}
-extension WaitingTable: Table {
-
- var allPlayers: [Player] {
- players as [Player]
- }
-
- var indexOfNextActor: Int {
- // The first player at the table starts the game
- 0
- }
-
- func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) {
+ override func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
// No cards playable while waiting
(.tableStateInvalid, nil)
}
diff --git a/Sources/App/Model/Tables/WeddingTable.swift b/Sources/App/Model/Tables/WeddingTable.swift
new file mode 100644
index 0000000..4f42fff
--- /dev/null
+++ b/Sources/App/Model/Tables/WeddingTable.swift
@@ -0,0 +1,121 @@
+import Foundation
+
+final class WeddingTable: AbstractTable {
+
+ var indexOfWeddingOffer: Int
+
+ init(table: BiddingTable, offerer: BiddingPlayer) {
+ let players = table.players.map { WeddingPlayer(player: $0, offersWedding: $0 == offerer) }
+ indexOfWeddingOffer = table.players.index(of: offerer)
+ super.init(table: table, players: players)
+ }
+
+ var hasRemainingActors: Bool {
+ players.contains { $0.requiresAction }
+ }
+
+ var requiresCardSelection: Bool {
+ players.contains { $0.selectsGame }
+ }
+
+ override var playedGame: GameType? {
+ .hochzeit
+ }
+
+ override func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard let player = players.player(named: name) else {
+ print("Player \(name) unexpectedly missing from wedding table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.canPerform(action) else {
+ return (.tableStateInvalid, nil)
+ }
+
+ switch action {
+ case .acceptWedding:
+ return performWeddingAccept(forPlayer: player)
+ case .withdrawFromAuction:
+ return performWithdrawl(forPlayer: player)
+ case .increaseOrMatchGame:
+ fatalError()
+ default:
+ return (.tableStateInvalid, nil)
+ }
+ }
+
+ private func performWeddingAccept(forPlayer player: WeddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard player.requiresAction else {
+ return (.tableStateInvalid, nil)
+ }
+ player.state = .wouldAcceptWedding
+ guard !hasRemainingActors else {
+ return (.success, nil)
+ }
+ // Nobody wants to play a higher game, so let the first player accept the wedding
+ players.first { $0.wouldAcceptWedding }!.selectsGame = true
+ return (.success, nil)
+ }
+
+ private func performWithdrawl(forPlayer player: WeddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard player.requiresAction else {
+ return (.tableStateInvalid, nil)
+ }
+ player.state = .withdrawnFromAuction
+ guard !hasRemainingActors else {
+ return (.success, nil)
+ }
+ // Nobody wants to play a higher game, so let the first player accept the wedding
+ guard let player = players.first(where: { $0.wouldAcceptWedding }) else {
+ // Nobody wants to accept the wedding or play something higher, so abort the game
+ let table = WaitingTable(oldTableAdvancedByOne: self)
+ return (.success, table)
+ }
+ player.selectsGame = true
+ return (.success, nil)
+ }
+
+ private func performOutbid(forPlayer player: WeddingPlayer) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard player.requiresAction else {
+ return (.tableStateInvalid, nil)
+ }
+ let table = BiddingTable(wedding: self, outbidBy: player)
+ return (.success, table)
+ }
+
+ override func playerData(at index: Int) -> (actions: [PlayerAction], games: [GameConvertible], cards: [PlayableCard], selectsGame: Bool) {
+ guard requiresCardSelection else {
+ return super.playerData(at: index)
+ }
+ let player = players[index]
+ guard player.selectsGame else {
+ return super.playerData(at: index)
+ }
+ return (actions: player.actions, games: [], cards: player.exchangeableCards, selectsGame: false)
+ }
+
+ override func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: ManageableTable?) {
+ guard requiresCardSelection else {
+ print("Wedding is not in stage where card should be selected")
+ return (.tableStateInvalid, nil)
+ }
+ guard let player = players.player(named: name) else {
+ print("Player \(name) unexpectedly missing from wedding table \(self.name)")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.selectsGame else {
+ print("Player \(name) is not the one selecting a wedding card")
+ return (.tableStateInvalid, nil)
+ }
+ guard player.canExchange(card: card) else {
+ print("Invalid card \(card) to exchange in wedding")
+ return (.tableStateInvalid, nil)
+ }
+
+ let offerer = players[indexOfWeddingOffer]
+ let trumpCard = offerer.replaceWeddingCard(with: card)
+ player.replace(card, with: trumpCard)
+ let table = PlayingTable(wedding: self, offeredBy: offerer, acceptedBy: player)
+ return (.success, table)
+ }
+
+}
diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift
index 18d00de..2583773 100644
--- a/Sources/App/configure.swift
+++ b/Sources/App/configure.swift
@@ -6,7 +6,8 @@ var database: Database!
public func configure(_ app: Application) throws {
// Set target environment
- app.environment = .development
+ app.environment = .production
+ app.logger.logLevel = .info // .notice
// serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))