import Foundation import WebSocketKit import Vapor import Fluent import Clairvoyant let maximumPlayersPerTable = 4 typealias TableId = String typealias TableName = String final class TableManagement { /// All tables indexed by their id private var tables = [UUID : ManageableTable]() /// The metric to log the current number of tables private var tableCountMetric: Metric /// The metric describing the number of players currently sitting at a table private let playingPlayerCountMetric: Metric /// The metric describing the number of players currently connected via a websocket private let connectedPlayerCountMetric: Metric /** Load the tables from a file in the storage folder - Throws: Errors when the file could not be read */ init(database: Database) async throws { self.tableCountMetric = .init( "schafkopf.tables", name: "Open tables", description: "The number of currently available tables") self.playingPlayerCountMetric = .init( "schafkopf.playing", name: "Sitting players", description: "The number of players currently sitting at a table") self.connectedPlayerCountMetric = .init( "schafkopf.connected", name: "Connected players", description: "The number of players with a websocket connection to the server") do { try await loadTables(from: database) } catch { log("Failed to load tables: \(error)") } } private func loadTables(from database: Database) async throws { try await Table.query(on: database).with(\.$players).all().forEach { table in guard !table.players.isEmpty else { _ = table.delete(on: database) return } let id = table.id! self.tables[id] = WaitingTable(id: id, name: table.name, isPublic: table.isPublic, players: table.players) } log("\(tables.count) tables loaded") await logTableCount() await logPlayingPlayerCount() await logConnectedPlayerCount() } private func logTableCount() async { _ = try? await tableCountMetric.update(tables.count) } private func logPlayingPlayerCount() async { let count = tables.values.sum { $0.playerCount } _ = try? await playingPlayerCountMetric.update(count) } private func logConnectedPlayerCount() async { let count = tables.values.sum { $0.numberOfConnectedPlayers } _ = try? await connectedPlayerCountMetric.update(count) } /** Create a new table with optional players. - Parameter name: The name of the table - Parameter players: The player creating the table - Parameter isPublic: Indicates that this is a game joinable by everyone - Returns: The table id */ func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) async throws -> TableInfo { let table = Table(name: name, isPublic: isPublic) try await table.create(on: database) player.$table.id = table.id try await player.update(on: database) let waitingTable = WaitingTable(newTable: table, user: player) self.tables[waitingTable.id] = waitingTable await logTableCount() await logPlayingPlayerCount() return waitingTable.tableInfo(forPlayer: player.name) } /// 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)?.tableInfo(forPlayer: player) } private func currentTable(for player: PlayerName) -> ManageableTable? { tables.values.first(where: { $0.playerNames.contains(player) }) } /** Join a table. - Parameter tableId: The table to join - Parameter player: The name of the player who wants to join. - Returns: The result of the join operation */ func join(tableId: UUID, player: User, in database: Database) async throws -> TableInfo { let table = try joinableTable(for: player, id: tableId) player.$table.id = table.id try await player.update(on: database) table.sendUpdateToAllPlayers() await logPlayingPlayerCount() return table.tableInfo(forPlayer: player.name) } private func joinableTable(for player: User, id tableId: UUID) throws -> ManageableTable { if let existing = self.currentTable(for: player.name) { guard existing.id == tableId else { throw Abort(.forbidden) // 403 } return existing } guard let table = self.tables[tableId] else { throw Abort(.gone) // 410 } guard let joinableTable = table as? WaitingTable, joinableTable.add(player: player.name, points: player.points) else { throw Abort(.expectationFailed) // 417 } return joinableTable } /** A player leaves the table it previously joined - Parameter player: The player leaving the table */ func leaveTable(player: User, in database: Database) async throws { guard let oldTable = currentTable(for: player.name) else { return } player.$table.id = nil guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else { tables[oldTable.id] = nil await logTableCount() try await player.update(on: database) try await Table.query(on: database).filter(\.$id == oldTable.id).delete() return } /// `player.canStartGame` is automatically set to false, because table is not full tables[table.id] = table #warning("Update points for all players, add penalty if running game") table.sendUpdateToAllPlayers() await logPlayingPlayerCount() await logConnectedPlayerCount() try await player.update(on: database) } func connect(player: PlayerName, using socket: WebSocket) async -> Bool { guard let table = currentTable(for: player) else { return false } let result = table.connect(player: player, using: socket) await logConnectedPlayerCount() return result } func disconnect(player: PlayerName) async { guard let table = currentTable(for: player) else { return } table.disconnect(player: player) await logConnectedPlayerCount() } func performAction(player: PlayerName, action: PlayerAction) -> PlayerActionResult { guard let table = currentTable(for: player) else { log("Player \(player) wants to \(action.id), but no table joined") return .noTableJoined } 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() if newTable is FinishedTable || newTable is DealingTable { // TODO: Save new table } return .success } func select(game: GameType, player: PlayerName) -> PlayerActionResult { guard let aTable = currentTable(for: player) else { log("Player \(player) wants to play \(game.rawValue), but no table joined") return .noTableJoined } 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 { log("Game selected by \(player), but no playing table \(table.name) created") table.sendUpdateToAllPlayers() return result } tables[newTable.id] = newTable newTable.sendUpdateToAllPlayers() // TODO: Save new table return .success } func play(card: Card, player: PlayerName, in database: Database) async throws -> PlayerActionResult { guard let table = currentTable(for: player) else { return .noTableJoined } 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 if let finished = newTable as? FinishedTable { try await finished.updatePlayerPoints(in: database) } newTable.sendUpdateToAllPlayers() return .success } func disconnectAllSockets() async { tables.values.forEach { $0.disconnectAllPlayers() } await logConnectedPlayerCount() } }