import Foundation import WebSocketKit import Vapor let maximumPlayersPerTable = 4 typealias TableId = String typealias TableName = String final class TableManagement: DiskWriter { /// All tables indexed by their id private var tables = [TableId : Table]() /// The handle to the file where the tables are persisted let storageFile: FileHandle /// The url to the file where the tables are persisted let storageFileUrl: URL /** Load the tables from a file in the storage folder - Parameter storageFolder: The url to the folder where the table file is stored - Throws: Errors when the file could not be read */ init(storageFolder: URL) throws { let url = storageFolder.appendingPathComponent("tables.txt") storageFileUrl = url storageFile = try Self.prepareFile(at: url) var entries = [TableId : (name: TableName, isPublic: 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, tableData in let table = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic) tableData.players.forEach { _ = table.add(player: $0) } tables[id] = table } print("Loaded \(tables.count) tables") } /** Writes the table info to disk. Currently only the id, name, visibility and players are stored, all other information is lost. - Parameter table: The changed table information to persist - Returns: `true`, if the entry was written, `false` on error */ @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: ":") return writeToDisk(line: entry) } /** Writes the deletion of a table to disk. The deletion is written as a separate entry and appended to the file, in order to reduce disk I/O. - Parameter tableId: The id of the deleted table - Returns: `true`, if the entry was written, `false` on error */ @discardableResult private func writeTableDeletionEntry(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 isPublic: Indicates that this is a game joinable by everyone - 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) tables[table.id] = table writeTableToDisk(table: table) 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)?.tableInfo(forPlayer: player) } private func currentTable(for player: PlayerName) -> Table? { tables.values.first(where: { $0.contains(player: 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: TableId, player: PlayerName) -> Result { if let existing = currentTable(for: player) { guard existing.id == tableId else { return .failure(.alreadyJoinedOtherTable) } return .success(existing.tableInfo(forPlayer: player)) } guard let table = tables[tableId] else { return .failure(.tableNotFound) } guard let joinableTable = table as? WaitingTable else { return .failure(.tableIsFull) } guard joinableTable.add(player: player) else { return .failure(.tableIsFull) } writeTableToDisk(table: table) joinableTable.sendUpdateToAllPlayers() return .success(joinableTable.tableInfo(forPlayer: player)) } /** A player leaves the table it previously joined - Parameter player: The name of the player */ func leaveTable(player: PlayerName) { guard let oldTable = currentTable(for: player) else { return } let table = WaitingTable(oldTable: oldTable, removing: player) tables[table.id] = table table.sendUpdateToAllPlayers() writeTableToDisk(table: table) } func connect(player: PlayerName, using socket: WebSocket) -> Bool { guard let table = currentTable(for: player) else { return false } return table.connect(player: player, using: socket) } func disconnect(player: PlayerName) { guard let table = currentTable(for: player) else { return } table.disconnect(player: player) } 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 } 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 aTable = currentTable(for: player) else { print("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 { 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) -> 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 newTable.sendUpdateToAllPlayers() return .success } }