diff --git a/Public/api.js b/Public/api.js index 43b06dd..50f3505 100644 --- a/Public/api.js +++ b/Public/api.js @@ -50,8 +50,7 @@ async function performGetPublicTablesRequest(token) { return fetch("/tables/public", { method: 'POST', body: token }) .then(convertServerResponse) .then(function(text) { - const decoded = atob(text) - return JSON.parse(decoded); + return JSON.parse(text); }) } @@ -78,4 +77,4 @@ function convertServerResponse(response) { default: throw Error("Unexpected response: " + response.statusText) } -} \ No newline at end of file +} diff --git a/Sources/App/Model/Database.swift b/Sources/App/Model/Database.swift index 20b2204..4b39fd9 100644 --- a/Sources/App/Model/Database.swift +++ b/Sources/App/Model/Database.swift @@ -1,23 +1,15 @@ import Foundation import Vapor -let playerPerTable = 4 - -typealias TableId = String -typealias TableName = String - final class Database { private let players: PlayerManagement private let tables: TableManagement - private var sessions: [SessionToken : WebSocket] - init() { self.players = PlayerManagement() self.tables = TableManagement() - self.sessions = [:] // TODO: Load server data from disk // TODO: Save data to disk } @@ -33,22 +25,19 @@ final class Database { } func deletePlayer(named name: PlayerName) { - if let sessionToken = players.deletePlayer(named: name) { - closeAndRemoveSession(for: sessionToken) - } - // TODO: Delete player from tables + _ = players.deletePlayer(named: name) + tables.remove(player: name) } func isValid(sessionToken token: SessionToken) -> Bool { players.isValid(sessionToken: token) } - func startSession(socket: WebSocket, sessionToken: SessionToken) { - closeAndRemoveSession(for: sessionToken) - sessions[sessionToken] = socket - socket.onText { [weak self] socket, text in - self?.didReceive(message: text, forSessionToken: sessionToken) + func startSession(socket: WebSocket, sessionToken: SessionToken) -> Bool { + guard let player = players.registeredPlayerExists(withSessionToken: sessionToken) else { + return false } + return tables.connect(player: player, using: socket) } private func didReceive(message: String, forSessionToken token: SessionToken) { @@ -56,13 +45,15 @@ final class Database { print("Session \(token.prefix(6)): \(message)") } - func endSession(forSessionToken token: SessionToken) { - players.endSession(forSessionToken: token) - closeAndRemoveSession(for: token) + func endSession(forSessionToken sessionToken: SessionToken) { + guard let player = players.endSession(forSessionToken: sessionToken) else { + return + } + closeSession(for: player) } - private func closeAndRemoveSession(for token: SessionToken) { - _ = sessions.removeValue(forKey: token)?.close() + private func closeSession(for player: PlayerName) { + tables.disconnect(player: player) } /** @@ -78,16 +69,12 @@ final class Database { players.registeredPlayerExists(withSessionToken: token) } + func currentTableOfPlayer(named player: PlayerName) -> TableId { + tables.currentTableOfPlayer(named: player) ?? "" + } + // MARK: Tables - func tableExists(withId id: TableId) -> Bool { - tables.tableExists(withId: id) - } - - func tableIsFull(withId id: TableId) -> Bool { - tables.tableIsFull(withId: id) - } - /** Create a new table with optional players. - Parameter name: The name of the table @@ -103,14 +90,7 @@ final class Database { tables.getPublicTableInfos() } - func join(tableId: TableId, player: PlayerName) { - let playersAtTable = tables.join(tableId: tableId, player: player) - playersAtTable - .compactMap { players.sessionToken(forPlayer: $0) } // Session Tokens - .compactMap { sessions[$0] } // Sockets - .forEach { socket in - // TODO: Notify sessions about changed players - // socket.send("") - } + func join(tableId: TableId, player: PlayerName) -> TableManagement.JoinTableResult { + tables.join(tableId: tableId, player: player) } } diff --git a/Sources/App/Model/PlayerManagement.swift b/Sources/App/Model/PlayerManagement.swift index 75f4e4a..b6b037f 100644 --- a/Sources/App/Model/PlayerManagement.swift +++ b/Sources/App/Model/PlayerManagement.swift @@ -86,11 +86,12 @@ final class PlayerManagement { return token } - func endSession(forSessionToken token: SessionToken) { + func endSession(forSessionToken token: SessionToken) -> PlayerName? { guard let player = playerNameForToken.removeValue(forKey: token) else { - return + return nil } sessionTokenForPlayer.removeValue(forKey: player) + return player } /** diff --git a/Sources/App/Model/TableManagement.swift b/Sources/App/Model/TableManagement.swift index b35184e..761d240 100644 --- a/Sources/App/Model/TableManagement.swift +++ b/Sources/App/Model/TableManagement.swift @@ -1,4 +1,10 @@ import Foundation +import WebSocketKit + +let maximumPlayersPerTable = 4 + +typealias TableId = String +typealias TableName = String final class TableManagement { @@ -14,16 +20,10 @@ final class TableManagement { /// A reverse list of players and their table id private var playerTables = [PlayerName: TableId]() + private var playerConnections = [PlayerName : WebSocket]() + init() { - - } - func tableExists(withId id: TableId) -> Bool { - tableNames[id] != nil - } - - func tableIsFull(withId id: TableId) -> Bool { - (tablePlayers[id]?.count ?? playerPerTable) < playerPerTable } /** @@ -52,24 +52,54 @@ final class TableManagement { }.sorted() } + func currentTableOfPlayer(named player: PlayerName) -> TableId? { + playerTables[player] + } + /** Join a table. - - Returns: The player names present at the table + - Returns: The result of the join operation */ - func join(tableId: TableId, player: PlayerName) -> [PlayerName] { + func join(tableId: TableId, player: PlayerName) -> JoinTableResult { guard var players = tablePlayers[tableId] else { - return [] + 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) + // TODO: End game if needed + // } tablePlayers[tableId] = players playerTables[tableId] = tableId - return players + return .success } func remove(player: PlayerName, fromTable tableId: TableId) { tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player } } + + func remove(player: PlayerName) { + fatalError() + } + + func connect(player: PlayerName, using socket: WebSocket) -> Bool { + fatalError() + } + + func disconnect(player: PlayerName) { + fatalError() + } + + enum JoinTableResult { + case tableNotFound + case tableIsFull + case success + } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 8ccee40..ea41da0 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -135,17 +135,34 @@ func routes(_ app: Application) throws { } /** - Start a new bidirectional session connection. + Get the current table of the player, if one exists. + - Parameter token: The session token of the player, as a string in the request body + - Throws: + - 400: Missing token + - 401: Invalid token + - Returns: The table id, or an empty string + */ + app.post("player", "table") { req -> String in + guard let token = req.body.string else { + throw Abort(.badRequest) // 400 + } + guard let player = database.registeredPlayerExists(withSessionToken: token) else { + throw Abort(.unauthorized) // 401 + } + return database.currentTableOfPlayer(named: player) + } + + /** + Start a new websocket connection for the client to receive table updates from the server - Returns: Nothing - - Note: The first message over the connection must be a valid session token. + - Note: The first (and only) message from the client over the connection must be a valid session token. */ app.webSocket("session", "start") { req, socket in socket.onText { socket, text in - guard database.isValid(sessionToken: text) else { + guard database.startSession(socket: socket, sessionToken: text) else { _ = socket.close() return } - database.startSession(socket: socket, sessionToken: text) } } @@ -198,7 +215,8 @@ func routes(_ app: Application) throws { throw Abort(.forbidden) // 403 } let list = database.getPublicTableInfos() - return try encoder.encode(list).base64EncodedString() + let data = try encoder.encode(list) + return String(data: data, encoding: .utf8)! } /** @@ -208,8 +226,8 @@ func routes(_ app: Application) throws { - Throws: - 400: Missing token - 401: The session token is invalid - - 404: The table id doesn't exist - - 406: The table is already full and can't be joined + - 410: The table id doesn't exist + - 417: The table is already full and can't be joined - Returns: Nothing */ app.post("table", "join", ":table") { req -> String in @@ -220,13 +238,13 @@ func routes(_ app: Application) throws { guard let player = database.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } - guard database.tableExists(withId: table) else { - throw Abort(.notFound) // 404 + switch database.join(tableId: table, player: player) { + case .tableNotFound: + throw Abort(.gone) // 410 + case .tableIsFull: + throw Abort(.expectationFailed) // 417 + case .success: + return "" } - guard !database.tableIsFull(withId: table) else { - throw Abort(.notAcceptable) // 406 - } - database.join(tableId: table, player: player) - return "" } }