import Foundation import WebSocketKit import Vapor let maximumPlayersPerTable = 4 typealias TableId = String typealias TableName = String final class TableManagement: DiskWriter { /// A list of table ids for public games private var publicTables = Set() /// A mapping from table id to table name (for all tables) private var tableNames = [TableId: TableName]() /// A mapping from table id to participating players private var tablePlayers = [TableId: [PlayerName]]() /// A reverse list of players and their table id private var playerTables = [PlayerName: TableId]() private var playerConnections = [PlayerName : WebSocket]() private var tablePhase = [TableId: GamePhase]() let storageFile: FileHandle let storageFileUrl: URL init(storageFolder: URL) throws { let url = storageFolder.appendingPathComponent("tables.txt") storageFileUrl = url storageFile = try Self.prepareFile(at: url) var entries = [TableId : (name: TableName, public: Bool, players: [PlayerName])]() try readLinesFromDisk().forEach { line in // Each line has parts: ID | NAME | PLAYER, PLAYER, ... let parts = line.components(separatedBy: ":") guard parts.count == 4 else { print("Invalid line in table file") return } let id = parts[0] let name = parts[1] let isPublic = parts[2] == "public" let players = parts[3].components(separatedBy: ",") if name == "" { entries[id] = nil } else { entries[id] = (name, isPublic, players) } } entries.forEach { id, table in tableNames[id] = table.name if table.public { publicTables.insert(id) } tablePlayers[id] = table.players tablePhase[id] = .waitingForPlayers for player in table.players { playerTables[player] = id } } print("Loaded \(tableNames.count) tables") } @discardableResult private func save(table tableId: TableId) -> Bool { let name = tableNames[tableId]! let visible = publicTables.contains(tableId) ? "public" : "private" let players = tablePlayers[tableId]! let entry = [tableId, name, visible, players.joined(separator: ",")].joined(separator: ":") return writeToDisk(line: entry) } @discardableResult private func deleteTable(tableId: TableId) -> Bool { let entry = [tableId, "", "", ""].joined(separator: ":") return writeToDisk(line: entry) } /** Create a new table with optional players. - Parameter name: The name of the table - Parameter players: The player creating the table - Parameter visible: Indicates that this is a game joinable by everyone - Returns: The table id */ func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId { let tableId = TableId.newToken() tableNames[tableId] = name tablePlayers[tableId] = [player] playerTables[player] = tableId if visible { publicTables.insert(tableId) } save(table: tableId) return tableId } func getPublicTableInfos() -> [TableInfo] { publicTables.map(tableInfo).sorted() } private func tableInfo(id tableId: TableId) -> TableInfo { let players = tablePlayers[tableId]!.map(playerState) return TableInfo( id: tableId, name: tableNames[tableId]!, players: players, tableIsFull: players.count == maximumPlayersPerTable) } private func playerState(_ player: PlayerName) -> TableInfo.PlayerState { .init(name: player, connected: playerIsConnected(player)) } private func playerIsConnected(_ player: PlayerName) -> Bool { playerConnections[player] != nil } func currentTableOfPlayer(named player: PlayerName) -> TableId? { playerTables[player] } /** Join a table. - Returns: The result of the join operation */ func join(tableId: TableId, player: PlayerName) -> JoinTableResult { guard var players = tablePlayers[tableId] else { return .tableNotFound } guard !players.contains(player) else { return .success } guard players.count < maximumPlayersPerTable else { return .tableIsFull } players.append(player) if let oldTable = playerTables[tableId] { remove(player: player, fromTable: oldTable) } tablePlayers[tableId] = players playerTables[player] = tableId save(table: tableId) return .success } func remove(player: PlayerName, fromTable tableId: TableId) { tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player } disconnect(player: player) playerTables[player] = nil // TODO: End game if needed // TODO: Remove table if empty save(table: tableId) } func remove(player: PlayerName) { guard let tableId = playerTables[player] else { return } // Already saves table to disk remove(player: player, fromTable: tableId) } func connect(player: PlayerName, using socket: WebSocket) -> Bool { guard let tableId = playerTables[player] else { return false } guard let players = tablePlayers[tableId] else { print("Player \(player) was assigned to missing table \(tableId.prefix(5))") playerTables[player] = nil return false } guard players.contains(player) else { print("Player \(player) wants updates for table \(tableId.prefix(5)) it didn't join") return false } playerConnections[player] = socket sendTableInfo(toTable: tableId) // TODO: Send cards to player return true } func disconnect(player: PlayerName) { if let socket = playerConnections.removeValue(forKey: player) { if !socket.isClosed { _ = socket.close() } } guard let tableId = playerTables[player] else { return } sendTableInfo(toTable: tableId) // Change table phase to waiting } private func sendTableInfo(toTable tableId: TableId) { let name = tableNames[tableId]! var players = tablePlayers[tableId]! let isFull = players.count == maximumPlayersPerTable for _ in players.count.. DealCardResult { guard let tableId = playerTables[player] else { return .noTableJoined } guard let players = tablePlayers[tableId] else { playerTables[player] = nil print("Player \(player) assigned to missing table \(tableId.prefix(5))") return .noTableJoined } guard players.count == maximumPlayersPerTable else { return .tableNotFull } let cards = Dealer.deal() let handCards = ["", "", "", ""] players.enumerated().forEach { index, player in guard let socket = playerConnections[player] else { return } let info = CardInfo( cards: cards[index].map { .init(card: $0.id, playable: false) }, tableCards: handCards.rotated(toStartAt: index)) socket.send(info) } return .success } }