From 289458bfd8d9f2142459f9bb995910f23c0be44c Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Thu, 9 Dec 2021 11:11:17 +0100 Subject: [PATCH] Refactor tables and players for clarity --- Sources/App/Infos/PlayerInfo.swift | 48 ++++--- Sources/App/Infos/TableInfo.swift | 38 +++--- Sources/App/Management/Database.swift | 4 +- Sources/App/Management/TableManagement.swift | 83 +++++++++--- Sources/App/Model/GameClass.swift | 13 +- .../App/Model/Players/AbstractPlayer.swift | 26 ++++ Sources/App/Model/Players/BiddingPlayer.swift | 51 ++++++++ Sources/App/Model/Players/DealingPlayer.swift | 24 ++++ Sources/App/Model/Players/Player.swift | 63 +++++++++ Sources/App/Model/Players/PlayingPlayer.swift | 30 +++++ Sources/App/Model/Players/WaitingPlayer.swift | 22 ++++ Sources/App/Model/Tables/AbstractTable.swift | 25 ++++ Sources/App/Model/Tables/BiddingTable.swift | 51 ++++++++ Sources/App/Model/Tables/DealingTable.swift | 39 ++++++ Sources/App/Model/Tables/PlayingTable.swift | 34 +++++ Sources/App/Model/Tables/Table.swift | 121 ++++++++++++++++++ Sources/App/Model/Tables/WaitingTable.swift | 110 ++++++++++++++++ 17 files changed, 718 insertions(+), 64 deletions(-) create mode 100644 Sources/App/Model/Players/AbstractPlayer.swift create mode 100644 Sources/App/Model/Players/BiddingPlayer.swift create mode 100644 Sources/App/Model/Players/DealingPlayer.swift create mode 100644 Sources/App/Model/Players/Player.swift create mode 100644 Sources/App/Model/Players/PlayingPlayer.swift create mode 100644 Sources/App/Model/Players/WaitingPlayer.swift create mode 100644 Sources/App/Model/Tables/AbstractTable.swift create mode 100644 Sources/App/Model/Tables/BiddingTable.swift create mode 100644 Sources/App/Model/Tables/DealingTable.swift create mode 100644 Sources/App/Model/Tables/PlayingTable.swift create mode 100644 Sources/App/Model/Tables/Table.swift create mode 100644 Sources/App/Model/Tables/WaitingTable.swift diff --git a/Sources/App/Infos/PlayerInfo.swift b/Sources/App/Infos/PlayerInfo.swift index 9bc23e6..3ee3608 100644 --- a/Sources/App/Infos/PlayerInfo.swift +++ b/Sources/App/Infos/PlayerInfo.swift @@ -1,40 +1,36 @@ import Foundation struct PlayerInfo: Codable, Equatable { - + + /// The name of the player let name: PlayerName - - let connected: Bool + + /// Indicates that the player is active, i.e. a session is established + let isConnected: Bool /// The player is the next one to perform an action - let active: Bool - - let selectsGame: Bool - - /// The cards in the hand of the player - let cards: [CardInfo] - - /// The action the player can perform - let actions: [String] + let isNextActor: Bool + /// The card which the player added to the current trick let playedCard: CardId? /// The height of the player card on the table stack - let position: Int - - init(player: Player, isMasked: Bool, trickPosition: Int) { + let positionInTrick: Int + + init(player: Player, isNextActor: Bool, position: Int) { self.name = player.name - self.connected = player.isConnected - self.active = player.isNextActor - self.selectsGame = player.selectsGame + self.isConnected = player.isConnected + self.isNextActor = isNextActor + self.positionInTrick = position self.playedCard = player.playedCard?.id - self.position = trickPosition - if isMasked { - self.cards = [] - self.actions = [] - } else { - self.actions = player.actions.map { $0.path } - self.cards = player.handCards.map { $0.cardInfo } - } + } + + /// Convert the property names into shorter strings for JSON encoding + enum CodingKeys: String, CodingKey { + case name = "name" + case isConnected = "connected" + case isNextActor = "active" + case playedCard = "card" + case positionInTrick = "position" } } diff --git a/Sources/App/Infos/TableInfo.swift b/Sources/App/Infos/TableInfo.swift index c59523a..33c0d5a 100644 --- a/Sources/App/Infos/TableInfo.swift +++ b/Sources/App/Infos/TableInfo.swift @@ -15,22 +15,30 @@ struct TableInfo: Codable { let playerRight: PlayerInfo? let playableGames: [GameId] - - init(_ table: Table, forPlayerAt playerIndex: Int) { - let player = table.player(at: playerIndex)! - self.id = table.id - self.name = table.name - self.player = table.playerInfo(at: playerIndex, masked: false)! - self.playerLeft = table.playerInfo(leftOf: playerIndex, masked: true) - self.playerAcross = table.playerInfo(acrossOf: playerIndex, masked: true) - self.playerRight = table.playerInfo(rightOf: playerIndex, masked: true) - if table.phase == .bidding || table.phase == .selectGame { - let games = table.minimumPlayableGame?.availableGames ?? GameType.allCases - self.playableGames = games.filter(player.canPlay).map { $0.id } - } else { - self.playableGames = [] - } + /// The cards in the hand of the player + let cards: [CardInfo] + + /// The action the player can perform + let actions: [String] + + let playerSelectsGame: Bool + + init(id: String, name: String, + own: PlayerInfo, left: PlayerInfo?, + across: PlayerInfo?, right: PlayerInfo?, + games: [GameId] = [], 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 + self.actions = actions.map { $0.path } + self.cards = cards.map { $0.cardInfo } + self.playerSelectsGame = selectGame } } diff --git a/Sources/App/Management/Database.swift b/Sources/App/Management/Database.swift index ce73070..ac03e9a 100644 --- a/Sources/App/Management/Database.swift +++ b/Sources/App/Management/Database.swift @@ -99,7 +99,7 @@ final class Database { return true } - func performAction(playerToken: SessionToken, action: Player.Action) -> PlayerActionResult { + func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { return .invalidToken } @@ -113,7 +113,7 @@ final class Database { return tables.select(game: game, player: player) } - func play(card: Card, playerToken: SessionToken) -> PlayCardResult { + func play(card: Card, playerToken: SessionToken) -> PlayerActionResult { guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else { return .invalidToken } diff --git a/Sources/App/Management/TableManagement.swift b/Sources/App/Management/TableManagement.swift index 4d34d4e..9a04c66 100644 --- a/Sources/App/Management/TableManagement.swift +++ b/Sources/App/Management/TableManagement.swift @@ -48,7 +48,7 @@ final class TableManagement: DiskWriter { } } entries.forEach { id, tableData in - let table = Table(id: id, name: tableData.name, isPublic: tableData.isPublic) + let table = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic) tableData.players.forEach { _ = table.add(player: $0) } tables[id] = table } @@ -65,8 +65,10 @@ final class TableManagement: DiskWriter { @discardableResult private func writeTableToDisk(table: Table) -> Bool { let visible = table.isPublic ? "public" : "private" - let players = table.playerNames.joined(separator: ",") - let entry = [table.id, table.name, visible, players].joined(separator: ":") + let players = table.playerNames + .joined(separator: ",") + let entry = [table.id, table.name, visible, players] + .joined(separator: ":") return writeToDisk(line: entry) } @@ -79,7 +81,8 @@ final class TableManagement: DiskWriter { */ @discardableResult private func writeTableDeletionEntry(tableId: TableId) -> Bool { - let entry = [tableId, "", "", ""].joined(separator: ":") + let entry = [tableId, "", "", ""] + .joined(separator: ":") return writeToDisk(line: entry) } @@ -91,27 +94,27 @@ final class TableManagement: DiskWriter { - Returns: The table id */ func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo { - let table = Table(newTable: name, isPublic: isPublic) + let table = WaitingTable(newTable: name, isPublic: isPublic) _ = table.add(player: player) tables[table.id] = table writeTableToDisk(table: table) - return table.compileInfo(for: player)! + return table.tableInfo(forPlayer: player) } /// A list of all public tables var publicTableList: [PublicTableInfo] { tables.values.filter { $0.isPublic }.map { $0.publicInfo } } - + /** Get the table info for a player - Parameter player: The name of the player - Returns: The table info, if the player has joined a table */ func tableInfo(player: PlayerName) -> TableInfo? { - currentTable(for: player)?.compileInfo(for: player) + currentTable(for: player)?.tableInfo(forPlayer: player) } - + private func currentTable(for player: PlayerName) -> Table? { tables.values.first(where: { $0.contains(player: player) }) } @@ -127,16 +130,20 @@ final class TableManagement: DiskWriter { guard existing.id == tableId else { return .failure(.alreadyJoinedOtherTable) } - return .success(existing.compileInfo(for: player)!) + return .success(existing.tableInfo(forPlayer: player)) } guard let table = tables[tableId] else { return .failure(.tableNotFound) } - guard table.add(player: player) else { + guard let joinableTable = table as? WaitingTable else { + return .failure(.tableIsFull) + } + guard joinableTable.add(player: player) else { return .failure(.tableIsFull) } writeTableToDisk(table: table) - return .success(table.compileInfo(for: player)!) + joinableTable.sendUpdateToAllPlayers() + return .success(joinableTable.tableInfo(forPlayer: player)) } /** @@ -144,10 +151,12 @@ final class TableManagement: DiskWriter { - Parameter player: The name of the player */ func leaveTable(player: PlayerName) { - guard let table = currentTable(for: player) else { + guard let oldTable = currentTable(for: player) else { return } - table.remove(player: player) + let table = WaitingTable(oldTable: oldTable, removing: player) + tables[table.id] = table + table.sendUpdateToAllPlayers() writeTableToDisk(table: table) } @@ -165,26 +174,60 @@ final class TableManagement: DiskWriter { table.disconnect(player: player) } - func performAction(player: PlayerName, action: Player.Action) -> PlayerActionResult { + func performAction(player: PlayerName, action: PlayerAction) -> PlayerActionResult { guard let table = currentTable(for: player) else { print("Player \(player) wants to \(action.path), but no table joined") return .noTableJoined } - return table.perform(action: action, forPlayer: player) + let (result, newTable) = table.perform(action: action, forPlayer: player) + guard result == .success else { + return result + } + guard let newTable = newTable else { + table.sendUpdateToAllPlayers() + return .success + } + tables[newTable.id] = newTable + newTable.sendUpdateToAllPlayers() + return .success } func select(game: GameType, player: PlayerName) -> PlayerActionResult { - guard let table = currentTable(for: player) else { + guard let aTable = currentTable(for: player) else { print("Player \(player) wants to play \(game.rawValue), but no table joined") return .noTableJoined } - return table.select(game: game, player: player) + guard let table = aTable as? BiddingTable else { + return .tableStateInvalid + } + let (result, newTable) = table.select(game: game, player: player) + guard result == .success else { + return result + } + guard let newTable = newTable else { + print("Game selected by \(player), but no playing table \(table.name) created") + table.sendUpdateToAllPlayers() + return result + } + tables[newTable.id] = newTable + newTable.sendUpdateToAllPlayers() + return .success } - func play(card: Card, player: PlayerName) -> PlayCardResult { + func play(card: Card, player: PlayerName) -> PlayerActionResult { guard let table = currentTable(for: player) else { return .noTableJoined } - return table.play(card: card, player: player) + let (result, newTable) = table.play(card: card, player: player) + guard result == .success else { + return result + } + guard let newTable = newTable else { + table.sendUpdateToAllPlayers() + return .success + } + tables[newTable.id] = newTable + newTable.sendUpdateToAllPlayers() + return .success } } diff --git a/Sources/App/Model/GameClass.swift b/Sources/App/Model/GameClass.swift index 6c850cc..8225bbe 100644 --- a/Sources/App/Model/GameClass.swift +++ b/Sources/App/Model/GameClass.swift @@ -18,6 +18,7 @@ extension GameType { } enum GameClass: Int { + case none = 0 case ruf = 1 case bettel = 2 case wenzGeier = 3 @@ -25,6 +26,7 @@ extension GameType { var cost: Int { switch self { + case .none: return 0 case .ruf: return 5 case .bettel: return 15 case .wenzGeier, .solo: return 20 @@ -38,9 +40,18 @@ extension GameType { self = .init(rawValue: rawValue + 1)! } + var allowsWedding: Bool { + switch self { + case .none, .ruf: + return true + default: + return false + } + } + var availableGames: [GameType] { switch self { - case .ruf: + case .none, .ruf: return GameType.allCases case .bettel: return [.bettel, .wenz, .geier, .soloEichel, .soloBlatt, .soloHerz, .soloSchelln] diff --git a/Sources/App/Model/Players/AbstractPlayer.swift b/Sources/App/Model/Players/AbstractPlayer.swift new file mode 100644 index 0000000..94fda94 --- /dev/null +++ b/Sources/App/Model/Players/AbstractPlayer.swift @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..dba8c94 --- /dev/null +++ b/Sources/App/Model/Players/BiddingPlayer.swift @@ -0,0 +1,51 @@ +import Foundation +import WebSocketKit + +final class BiddingPlayer { + + let name: String + + var socket: WebSocket? + + let cards: [PlayableCard] + + var isStillBidding = true + + var isAllowedToOfferWedding = true + + var offersWedding = 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 + } + + var rawCards: [Card] { + cards.map { $0.card } + } + + + var actions: [PlayerAction] { + guard isStillBidding else { + return [] + } + guard canOfferWedding, isAllowedToOfferWedding, !offersWedding else { + return [.increaseOrMatchGame, .withdrawFromAuction] + } + return [.increaseOrMatchGame, .withdrawFromAuction, .offerWedding] + } + + var playedCard: Card? { + nil + } +} diff --git a/Sources/App/Model/Players/DealingPlayer.swift b/Sources/App/Model/Players/DealingPlayer.swift new file mode 100644 index 0000000..fd3cac2 --- /dev/null +++ b/Sources/App/Model/Players/DealingPlayer.swift @@ -0,0 +1,24 @@ +import Foundation +import WebSocketKit + +final class DealingPlayer: AbstractPlayer { + + var cards: [PlayableCard] = [] + + var didDouble: Bool? = nil + + init(player: WaitingPlayer) { + super.init(player: player) + } +} + +extension DealingPlayer: Player { + + var actions: [PlayerAction] { + didDouble == nil ? [.initialDoubleCost, .noDoubleCost] : [] + } + + var playedCard: Card? { + nil + } +} diff --git a/Sources/App/Model/Players/Player.swift b/Sources/App/Model/Players/Player.swift new file mode 100644 index 0000000..79b7182 --- /dev/null +++ b/Sources/App/Model/Players/Player.swift @@ -0,0 +1,63 @@ +import Foundation +import WebSocketKit + +protocol Player: AnyObject { + + var name: String { get } + + var socket: WebSocket? { get set } + + var playedCard: Card? { get } + + var actions: [PlayerAction] { get } + + var cards: [PlayableCard] { get } + +} + +extension Player { + + // MARK: Connection + + /// Indicate that the player is connected when at a table + var isConnected: Bool { + guard let socket = socket else { + return false + } + guard !socket.isClosed else { + self.socket = nil + return false + } + return true + } + + func connect(using socket: WebSocket) { + _ = self.socket?.close() + self.socket = socket + } + + func disconnect() -> Bool { + guard let socket = socket else { + return false + } + do { + try socket.close().wait() + } catch { + print("Failed to close socket for player: \(name): \(error)") + } + self.socket = nil + return true + } + + + func send(_ info: TableInfo) { + try? socket?.send(encodeJSON(info)) + } + + // MARK: Actions + + func canPerform(_ action: PlayerAction) -> Bool { + actions.contains(action) + } + +} diff --git a/Sources/App/Model/Players/PlayingPlayer.swift b/Sources/App/Model/Players/PlayingPlayer.swift new file mode 100644 index 0000000..3b0088d --- /dev/null +++ b/Sources/App/Model/Players/PlayingPlayer.swift @@ -0,0 +1,30 @@ +import Foundation +import WebSocketKit + +final class PlayingPlayer: AbstractPlayer { + + var playedCard: Card? = nil + + var cards: [PlayableCard] + + var leadsGame = false + + var canStillRaise = true + + init(player: BiddingPlayer) { + self.cards = player.cards + super.init(player: player) + } +} + +extension PlayingPlayer: Player { + + + var actions: [PlayerAction] { + guard canStillRaise, !leadsGame else { + return [] + } + return [.doubleDuringGame] + } + +} diff --git a/Sources/App/Model/Players/WaitingPlayer.swift b/Sources/App/Model/Players/WaitingPlayer.swift new file mode 100644 index 0000000..19ac291 --- /dev/null +++ b/Sources/App/Model/Players/WaitingPlayer.swift @@ -0,0 +1,22 @@ +import Foundation +import WebSocketKit + +final class WaitingPlayer: AbstractPlayer { + + var canStartGame: Bool = false +} + +extension WaitingPlayer: Player { + + var actions: [PlayerAction] { + canStartGame ? [.deal] : [] + } + + var cards: [PlayableCard] { + [] + } + + var playedCard: Card? { + nil + } +} diff --git a/Sources/App/Model/Tables/AbstractTable.swift b/Sources/App/Model/Tables/AbstractTable.swift new file mode 100644 index 0000000..d9ab4c8 --- /dev/null +++ b/Sources/App/Model/Tables/AbstractTable.swift @@ -0,0 +1,25 @@ +import Foundation + +class AbstractTable { + + /// The unique id of the table + let id: TableId + + /// The name of the table + let name: TableName + + /// Indicates that the table is visible to all players, and can be joined by anyone + let isPublic: Bool + + init(table: AbstractTable) { + self.id = table.id + self.name = table.name + self.isPublic = table.isPublic + } + + init(id: TableId, name: TableName, isPublic: Bool) { + self.id = id + self.name = name + self.isPublic = isPublic + } +} diff --git a/Sources/App/Model/Tables/BiddingTable.swift b/Sources/App/Model/Tables/BiddingTable.swift new file mode 100644 index 0000000..3ab5c74 --- /dev/null +++ b/Sources/App/Model/Tables/BiddingTable.swift @@ -0,0 +1,51 @@ +import Foundation + +final class BiddingTable: AbstractTable { + + var players: [BiddingPlayer] + + var hasSelectedGame: Bool { + // TODO: Implement + false + } + + init(table: DealingTable) { + self.players = table.players.map { + BiddingPlayer(player: $0, cards: []) + } + super.init(table: table) + } + + func select(game: GameType, player: PlayerName) -> (result: PlayerActionResult, table: Table?) { + // TODO: Implement + return (.tableStateInvalid, nil) + } + + func makePlayingTable() -> PlayingTable { + // TODO: Implement + fatalError() + } +} + +extension BiddingTable: Table { + + var allPlayers: [Player] { + players + } + + var indexOfNextActor: Int { + // TODO: Implement + return 0 + } + + func perform(action: PlayerAction, forPlayer: PlayerName) -> (result: PlayerActionResult, table: Table?) { + // TODO: Implement bidding actions + return (.tableStateInvalid, nil) + } + + func play(card: Card, player name: PlayerName) -> (result: PlayerActionResult, table: Table?) { + // TODO: Implement for wedding + return (.tableStateInvalid, nil) + } + +} diff --git a/Sources/App/Model/Tables/DealingTable.swift b/Sources/App/Model/Tables/DealingTable.swift new file mode 100644 index 0000000..219e8bb --- /dev/null +++ b/Sources/App/Model/Tables/DealingTable.swift @@ -0,0 +1,39 @@ +import Foundation + +final class DealingTable: AbstractTable { + + var players: [DealingPlayer] + + 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) } + } + } +} + +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) + } + +} diff --git a/Sources/App/Model/Tables/PlayingTable.swift b/Sources/App/Model/Tables/PlayingTable.swift new file mode 100644 index 0000000..04ee339 --- /dev/null +++ b/Sources/App/Model/Tables/PlayingTable.swift @@ -0,0 +1,34 @@ +import Foundation + +final class PlayingTable: AbstractTable { + + var players: [PlayingPlayer] + + init(table: BiddingTable) { + self.players = table.players.map(PlayingPlayer.init) + super.init(table: 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 new file mode 100644 index 0000000..f592829 --- /dev/null +++ b/Sources/App/Model/Tables/Table.swift @@ -0,0 +1,121 @@ +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 new file mode 100644 index 0000000..4ba9bc3 --- /dev/null +++ b/Sources/App/Model/Tables/WaitingTable.swift @@ -0,0 +1,110 @@ +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] = [] + + /// 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) + } + + /** + Create a new table. + - 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) + } + + /** + Convert another table to a waiting table. + + 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. + */ + init(oldTable: Table, removing player: PlayerName) { + self.players = oldTable.allPlayers + .filter { $0.name != player } + .map(WaitingPlayer.init) + super.init(table: oldTable) + } + + /** + Add a player to the table. + - Parameter player: The name of the player to add + - Returns: `true`, if the player could be added, `false` if the table is full + */ + func add(player: PlayerName) -> Bool { + guard !isFull else { + return false + } + let player = WaitingPlayer(name: player) + players.append(player) + // Allow dealing of cards if table is full + if isFull { + players.forEach { $0.canStartGame = true } + } + return true + } + + /** + Perform an action on the waiting table. + + Only dealing is a valid action (if the table is full) + - Parameter action: The action to perform + - Parameter player: The name of the player + */ + func perform(action: PlayerAction, forPlayer name: PlayerName) -> (result: PlayerActionResult, table: Table?) { + // Only dealing is allowed... + guard action == .deal else { + return (.tableStateInvalid, nil) + } + // and only when table is full + guard isFull else { + return (.tableStateInvalid, nil) + } + guard let player = player(named: name) else { + print("Unexpected action \(action) for missing player \(name) at table \(self.name)") + return (.tableStateInvalid, nil) + } + + guard player.canPerform(.deal) else { + print("Player \(name) cant perform deal, although table is full") + return (.tableStateInvalid, nil) + } + 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?) { + // No cards playable while waiting + (.tableStateInvalid, nil) + } +}