import Foundation import Crypto let playerPerTable = 4 final class Database { /// A mapping between usernames and their password hashes private var userPasswordHashes = [String: String]() /// A mapping between usernames and generated access tokens for a session private var authTokenForUser = [String: String]() /// A reverse mapping between generated access tokens and usernames private var userForToken = [String: String]() /// 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 = [String: String]() /// A mapping from table id to participating players private var tablePlayers = [String: [String]]() /// A reverse list of players and their table id private var playerTables = [String: String]() init() { } /** Check if a user exists. - Parameter name: The name of the user - Returns: true, if the user exists */ func has(user: String) -> Bool { userPasswordHashes[user] != nil } /** Get the password hash for a user, if the user exists. - Parameter name: The name of the user - Returns: The stored password hash, if the user exists */ func hash(ofUser name: String) -> String? { userPasswordHashes[name] } /** Create a new user and assign an access token. - Parameter name: The name of the new user - Parameter hash: The password hash of the user - Returns: The generated access token for the session */ func add(user name: String, hash: String) -> String { self.userPasswordHashes[name] = hash return startSession(forUser: name) } /** Start a new session for an existing user. - Parameter name: The user name - Returns: The generated access token for the session */ func startSession(forUser name: String) -> String { let token = newToken() self.authTokenForUser[name] = token self.userForToken[token] = name return token } /** Get the user for a session token. - Parameter token: The access token for the user - Returns: The name of the user, if it exists */ func user(forToken token: String) -> String? { userForToken[token] } func tableExists(named name: String) -> Bool { tableNames.contains { $0.value == name } } func tableExists(withId id: String) -> Bool { tableNames[id] != nil } func tableIsFull(withId id: String) -> Bool { tablePlayers[id]!.count < playerPerTable } /** 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: String, player: String, visible: Bool) -> String { let tableId = newToken() tableNames[tableId] = tableId tablePlayers[tableId] = [player] playerTables[player] = tableId if visible { publicTables.insert(tableId) } return tableId } func getPublicTableInfos() -> [TableInfo] { publicTables.map { tableId in TableInfo(id: tableId, name: tableNames[tableId]!, players: tablePlayers[tableId]!) }.sorted() } func join(tableId: String, player: String) { tablePlayers[tableId]!.append(player) if let oldTable = playerTables[tableId] { remove(player: player, fromTable: oldTable) } playerTables[tableId] = tableId } func remove(player: String, fromTable tableId: String) { tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player } } /** Create a new access token. */ private func newToken() -> String { Crypto.SymmetricKey.init(size: .bits128).withUnsafeBytes { $0.hexEncodedString() } } }