Switch to SQLite database over text files
This commit is contained in:
parent
86456b2441
commit
5eafcfdf4d
@ -1,122 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import Vapor
|
|
||||||
|
|
||||||
final class Database {
|
|
||||||
|
|
||||||
private let players: PlayerManagement
|
|
||||||
|
|
||||||
private let tables: TableManagement
|
|
||||||
|
|
||||||
init(storageFolder: URL) throws {
|
|
||||||
self.players = try PlayerManagement(storageFolder: storageFolder)
|
|
||||||
self.tables = try TableManagement(storageFolder: storageFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Players & Sessions
|
|
||||||
|
|
||||||
func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
|
|
||||||
players.registerPlayer(named: name, hash: hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func passwordHashForExistingPlayer(named name: PlayerName) -> PasswordHash? {
|
|
||||||
players.passwordHash(ofRegisteredPlayer: name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func deletePlayer(named name: PlayerName) {
|
|
||||||
_ = players.deletePlayer(named: name)
|
|
||||||
tables.leaveTable(player: name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValid(sessionToken token: SessionToken) -> Bool {
|
|
||||||
players.isValid(sessionToken: token)
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
// TODO: Handle client requests
|
|
||||||
print("Session \(token.prefix(6)): \(message)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func endSession(forSessionToken sessionToken: SessionToken) {
|
|
||||||
guard let player = players.endSession(forSessionToken: sessionToken) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tables.disconnect(player: player)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Start a new session for an existing user.
|
|
||||||
- Parameter name: The user name
|
|
||||||
- Returns: The generated access token for the session
|
|
||||||
*/
|
|
||||||
func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
|
|
||||||
players.startNewSessionForRegisteredPlayer(named: name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
|
|
||||||
players.registeredPlayerExists(withSessionToken: token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func currentTableOfPlayer(named player: PlayerName) -> TableInfo? {
|
|
||||||
tables.tableInfo(player: player)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Tables
|
|
||||||
|
|
||||||
/**
|
|
||||||
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 {
|
|
||||||
tables.createTable(named: name, player: player, isPublic: isPublic)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPublicTableInfos() -> [PublicTableInfo] {
|
|
||||||
tables.publicTableList
|
|
||||||
}
|
|
||||||
|
|
||||||
func join(tableId: TableId, playerToken: SessionToken) -> Result<TableInfo,JoinTableResult> {
|
|
||||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
|
||||||
return .failure(.invalidToken)
|
|
||||||
}
|
|
||||||
return tables.join(tableId: tableId, player: player)
|
|
||||||
}
|
|
||||||
|
|
||||||
func leaveTable(playerToken: SessionToken) -> Bool {
|
|
||||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
tables.leaveTable(player: player)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult {
|
|
||||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
|
||||||
return .invalidToken
|
|
||||||
}
|
|
||||||
return tables.performAction(player: player, action: action)
|
|
||||||
}
|
|
||||||
|
|
||||||
func select(game: GameType, playerToken: SessionToken) -> PlayerActionResult {
|
|
||||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
|
||||||
return .invalidToken
|
|
||||||
}
|
|
||||||
return tables.select(game: game, player: player)
|
|
||||||
}
|
|
||||||
|
|
||||||
func play(card: Card, playerToken: SessionToken) -> PlayerActionResult {
|
|
||||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
|
||||||
return .invalidToken
|
|
||||||
}
|
|
||||||
return tables.play(card: card, player: player)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
//
|
|
||||||
// File.swift
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// Created by iMac on 01.12.21.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
|
|
||||||
protocol DiskWriter: AnyObject {
|
|
||||||
|
|
||||||
var storageFile: FileHandle { get set }
|
|
||||||
|
|
||||||
var storageFileUrl: URL { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension DiskWriter {
|
|
||||||
|
|
||||||
func replaceFile(data: String) throws {
|
|
||||||
let data = data.data(using: .utf8)!
|
|
||||||
try storageFile.close()
|
|
||||||
try data.write(to: storageFileUrl)
|
|
||||||
storageFile = try FileHandle(forUpdating: storageFileUrl)
|
|
||||||
if #available(macOS 10.15.4, *) {
|
|
||||||
try storageFile.seekToEnd()
|
|
||||||
} else {
|
|
||||||
storageFile.seekToEndOfFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func prepareFile(at url: URL) throws -> FileHandle {
|
|
||||||
if !FileManager.default.fileExists(atPath: url.path) {
|
|
||||||
try Data().write(to: url)
|
|
||||||
}
|
|
||||||
return try FileHandle(forUpdating: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeToDisk(line: String) -> Bool {
|
|
||||||
let data = (line + "\n").data(using: .utf8)!
|
|
||||||
do {
|
|
||||||
if #available(macOS 10.15.4, *) {
|
|
||||||
try storageFile.write(contentsOf: data)
|
|
||||||
} else {
|
|
||||||
storageFile.write(data)
|
|
||||||
}
|
|
||||||
try storageFile.synchronize()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
print("Failed to save data to file: \(storageFileUrl.path): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readDataFromDisk() throws -> Data {
|
|
||||||
if #available(macOS 10.15.4, *) {
|
|
||||||
guard let data = try storageFile.readToEnd() else {
|
|
||||||
try storageFile.seekToEnd()
|
|
||||||
return Data()
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
} else {
|
|
||||||
return storageFile.readDataToEndOfFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readLinesFromDisk() throws -> [String] {
|
|
||||||
let data = try readDataFromDisk()
|
|
||||||
return parseLines(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func parseLines(data: Data) -> [String] {
|
|
||||||
String(data: data, encoding: .utf8)!
|
|
||||||
.components(separatedBy: "\n")
|
|
||||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
||||||
.filter { $0 != "" }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,164 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
typealias PlayerName = String
|
|
||||||
typealias PasswordHash = String
|
|
||||||
typealias SessionToken = String
|
|
||||||
|
|
||||||
/// Manages player registration, session tokens and password hashes
|
|
||||||
final class PlayerManagement: DiskWriter {
|
|
||||||
|
|
||||||
/// A mapping between player name and their password hashes
|
|
||||||
private var playerPasswordHashes = [PlayerName: PasswordHash]()
|
|
||||||
|
|
||||||
/// A mapping between player name and generated access tokens for a session
|
|
||||||
private var sessionTokenForPlayer = [PlayerName: SessionToken]()
|
|
||||||
|
|
||||||
/// A reverse mapping between generated access tokens and player name
|
|
||||||
private var playerNameForToken = [SessionToken: PlayerName]()
|
|
||||||
|
|
||||||
var storageFile: FileHandle
|
|
||||||
|
|
||||||
let storageFileUrl: URL
|
|
||||||
|
|
||||||
init(storageFolder: URL) throws {
|
|
||||||
let url = storageFolder.appendingPathComponent("passwords.txt")
|
|
||||||
|
|
||||||
storageFileUrl = url
|
|
||||||
storageFile = try Self.prepareFile(at: url)
|
|
||||||
|
|
||||||
var redundantEntries = 0
|
|
||||||
try readLinesFromDisk().forEach { line in
|
|
||||||
let parts = line.components(separatedBy: ":")
|
|
||||||
// Token may contain the separator
|
|
||||||
guard parts.count >= 2 else {
|
|
||||||
print("Invalid line in password file")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let name = parts[0]
|
|
||||||
let token = parts.dropFirst().joined(separator: ":")
|
|
||||||
if token == "" {
|
|
||||||
playerPasswordHashes[name] = nil
|
|
||||||
redundantEntries += 2 // One for creation, one for deletion
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if playerPasswordHashes[name] != nil {
|
|
||||||
redundantEntries += 1
|
|
||||||
}
|
|
||||||
playerPasswordHashes[name] = token
|
|
||||||
}
|
|
||||||
|
|
||||||
let playerCount = playerPasswordHashes.count
|
|
||||||
let totalEntries = playerCount + redundantEntries
|
|
||||||
let percentage = playerCount * 100 / totalEntries
|
|
||||||
print("Loaded \(playerCount) players from \(totalEntries) entries (\(percentage) % useful)")
|
|
||||||
if percentage < 80 && redundantEntries > 10 {
|
|
||||||
try optimizePlayerFile()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func optimizePlayerFile() throws {
|
|
||||||
print("Optimizing player file...")
|
|
||||||
let lines = playerPasswordHashes.map { $0.key + ":" + $0.value + "\n" }.joined()
|
|
||||||
try replaceFile(data: lines)
|
|
||||||
print("Done.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func save(password: PasswordHash, forPlayer player: PlayerName) -> Bool {
|
|
||||||
writeToDisk(line: player + ":" + password)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func deletePassword(forPlayer player: PlayerName) -> Bool {
|
|
||||||
writeToDisk(line: player + ":")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Check if a player exists.
|
|
||||||
- Parameter name: The name of the player
|
|
||||||
- Returns: true, if the player exists
|
|
||||||
*/
|
|
||||||
func hasRegisteredPlayer(named user: PlayerName) -> Bool {
|
|
||||||
playerPasswordHashes[user] != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Get the password hash for a player, if the player exists.
|
|
||||||
- Parameter name: The name of the player
|
|
||||||
- Returns: The stored password hash, if the player exists
|
|
||||||
*/
|
|
||||||
func passwordHash(ofRegisteredPlayer name: PlayerName) -> PasswordHash? {
|
|
||||||
playerPasswordHashes[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Create a new player and assign an access token.
|
|
||||||
- Parameter name: The name of the new player
|
|
||||||
- Parameter hash: The password hash of the player
|
|
||||||
- Returns: The generated access token for the session
|
|
||||||
*/
|
|
||||||
func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
|
|
||||||
guard !hasRegisteredPlayer(named: name) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard save(password: hash, forPlayer: name) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.playerPasswordHashes[name] = hash
|
|
||||||
return startNewSessionForRegisteredPlayer(named: name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Delete a player
|
|
||||||
- Parameter name: The name of the player to delete.
|
|
||||||
- Returns: The session token of the current player, if one exists
|
|
||||||
*/
|
|
||||||
func deletePlayer(named name: PlayerName) -> SessionToken? {
|
|
||||||
guard deletePassword(forPlayer: name) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
playerPasswordHashes.removeValue(forKey: name)
|
|
||||||
guard let sessionToken = sessionTokenForPlayer.removeValue(forKey: name) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
playerNameForToken.removeValue(forKey: sessionToken)
|
|
||||||
return sessionToken
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValid(sessionToken token: SessionToken) -> Bool {
|
|
||||||
playerNameForToken[token] != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessionToken(forPlayer player: PlayerName) -> SessionToken? {
|
|
||||||
sessionTokenForPlayer[player]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Start a new session for an existing player.
|
|
||||||
- Parameter name: The player name
|
|
||||||
- Returns: The generated access token for the session
|
|
||||||
*/
|
|
||||||
func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
|
|
||||||
let token = SessionToken.newToken()
|
|
||||||
self.sessionTokenForPlayer[name] = token
|
|
||||||
self.playerNameForToken[token] = name
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
func endSession(forSessionToken token: SessionToken) -> PlayerName? {
|
|
||||||
guard let player = playerNameForToken.removeValue(forKey: token) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
sessionTokenForPlayer.removeValue(forKey: player)
|
|
||||||
return player
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Get the player for a session token.
|
|
||||||
- Parameter token: The access token for the player
|
|
||||||
- Returns: The name of the player, if it exists
|
|
||||||
*/
|
|
||||||
func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
|
|
||||||
playerNameForToken[token]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,109 +1,36 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import WebSocketKit
|
import WebSocketKit
|
||||||
import Vapor
|
import Vapor
|
||||||
|
import Fluent
|
||||||
|
|
||||||
let maximumPlayersPerTable = 4
|
let maximumPlayersPerTable = 4
|
||||||
|
|
||||||
typealias TableId = String
|
typealias TableId = String
|
||||||
typealias TableName = String
|
typealias TableName = String
|
||||||
|
|
||||||
final class TableManagement: DiskWriter {
|
final class TableManagement {
|
||||||
|
|
||||||
/// All tables indexed by their id
|
/// All tables indexed by their id
|
||||||
private var tables = [TableId : ManageableTable]()
|
private var tables = [UUID : ManageableTable]()
|
||||||
|
|
||||||
/// The handle to the file where the tables are persisted
|
|
||||||
var 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
|
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
|
- Throws: Errors when the file could not be read
|
||||||
*/
|
*/
|
||||||
init(storageFolder: URL) throws {
|
init(db: Database) throws {
|
||||||
let url = storageFolder.appendingPathComponent("tables.txt")
|
Table.query(on: db).with(\.$players).all().whenSuccess { loadedTables in
|
||||||
|
for table in loadedTables {
|
||||||
storageFileUrl = url
|
guard !table.players.isEmpty else {
|
||||||
storageFile = try Self.prepareFile(at: url)
|
_ = table.delete(on: db)
|
||||||
|
continue
|
||||||
var entries = [TableId : (name: TableName, isPublic: Bool, players: [PlayerName])]()
|
}
|
||||||
var redundantEntries = 0
|
let id = table.id!
|
||||||
try readLinesFromDisk().forEach { line in
|
self.tables[id] = WaitingTable(id: id, name: table.name, isPublic: table.isPublic, players: table.players)
|
||||||
// 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]
|
print("\(self.tables.count) tables loaded")
|
||||||
let name = parts[1]
|
|
||||||
let isPublic = parts[2] == "public"
|
|
||||||
let players = parts[3].components(separatedBy: ",")
|
|
||||||
if name == "" {
|
|
||||||
entries[id] = nil
|
|
||||||
redundantEntries += 2 // One for creation, one for deletion
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if entries[id] != nil {
|
|
||||||
redundantEntries += 1
|
|
||||||
}
|
|
||||||
entries[id] = (name, isPublic, players)
|
|
||||||
}
|
|
||||||
entries.forEach { id, tableData in
|
|
||||||
tables[id] = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic, players: tableData.players)
|
|
||||||
}
|
|
||||||
let totalEntries = entries.count + redundantEntries
|
|
||||||
let percentage = entries.count * 100 / totalEntries
|
|
||||||
print("Loaded \(tables.count) tables from \(totalEntries) entries (\(percentage) % useful)")
|
|
||||||
if percentage < 80 && redundantEntries > 10 {
|
|
||||||
try optimizeTableFile()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func optimizeTableFile() throws {
|
|
||||||
print("Optimizing tables file...")
|
|
||||||
let lines = tables.values.map(entry).joined(separator: "\n") + "\n"
|
|
||||||
try replaceFile(data: lines)
|
|
||||||
print("Done.")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func entry(for table: ManageableTable) -> String {
|
|
||||||
let visible = table.isPublic ? "public" : "private"
|
|
||||||
let players = table.playerNames
|
|
||||||
.joined(separator: ",")
|
|
||||||
return [table.id, table.name, visible, players].joined(separator: ":")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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: ManageableTable) -> Bool {
|
|
||||||
let entry = entry(for: table)
|
|
||||||
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.
|
Create a new table with optional players.
|
||||||
- Parameter name: The name of the table
|
- Parameter name: The name of the table
|
||||||
@ -111,11 +38,19 @@ final class TableManagement: DiskWriter {
|
|||||||
- Parameter isPublic: Indicates that this is a game joinable by everyone
|
- Parameter isPublic: Indicates that this is a game joinable by everyone
|
||||||
- Returns: The table id
|
- Returns: The table id
|
||||||
*/
|
*/
|
||||||
func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo {
|
func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) -> EventLoopFuture<TableInfo> {
|
||||||
let table = WaitingTable(newTable: name, isPublic: isPublic, creator: player)
|
let table = Table(name: name, isPublic: isPublic)
|
||||||
tables[table.id] = table
|
return table.create(on: database).flatMap {
|
||||||
writeTableToDisk(table: table)
|
player.$table.id = table.id
|
||||||
return table.tableInfo(forPlayer: player)
|
return player.update(on: database)
|
||||||
|
}.flatMap {
|
||||||
|
Table.query(on: database).with(\.$players).filter(\.$id == table.id!).first()
|
||||||
|
}.unwrap(or: Abort(.notFound))
|
||||||
|
.map { storedTable in
|
||||||
|
let table = WaitingTable(newTable: storedTable)
|
||||||
|
self.tables[table.id] = table
|
||||||
|
return table.tableInfo(forPlayer: player.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of all public tables
|
/// A list of all public tables
|
||||||
@ -142,40 +77,46 @@ final class TableManagement: DiskWriter {
|
|||||||
- Parameter player: The name of the player who wants to join.
|
- Parameter player: The name of the player who wants to join.
|
||||||
- Returns: The result of the join operation
|
- Returns: The result of the join operation
|
||||||
*/
|
*/
|
||||||
func join(tableId: TableId, player: PlayerName) -> Result<TableInfo, JoinTableResult> {
|
func join(tableId: UUID, player: User, in database: Database) -> EventLoopFuture<TableInfo> {
|
||||||
if let existing = currentTable(for: player) {
|
return database.eventLoop.future().flatMapThrowing { _ -> ManageableTable in
|
||||||
guard existing.id == tableId else {
|
if let existing = self.currentTable(for: player.name) {
|
||||||
return .failure(.alreadyJoinedOtherTable)
|
guard existing.id == tableId else {
|
||||||
|
throw Abort(.forbidden) // 403
|
||||||
|
}
|
||||||
|
return existing
|
||||||
}
|
}
|
||||||
return .success(existing.tableInfo(forPlayer: player))
|
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
|
||||||
|
}.flatMap { table -> EventLoopFuture<ManageableTable> in
|
||||||
|
player.$table.id = table.id
|
||||||
|
return player.update(on: database).map { table }
|
||||||
|
}.map { table in
|
||||||
|
table.sendUpdateToAllPlayers()
|
||||||
|
return table.tableInfo(forPlayer: player.name)
|
||||||
}
|
}
|
||||||
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
|
A player leaves the table it previously joined
|
||||||
- Parameter player: The name of the player
|
- Parameter player: The player leaving the table
|
||||||
*/
|
*/
|
||||||
func leaveTable(player: PlayerName) {
|
func leaveTable(player: User, in database: Database) -> EventLoopFuture<Void> {
|
||||||
guard let oldTable = currentTable(for: player) else {
|
guard let oldTable = currentTable(for: player.name) else {
|
||||||
return
|
return database.eventLoop.makeSucceededVoidFuture()
|
||||||
}
|
}
|
||||||
/// `player.canStartGame` is automatically set to false, because table is not full
|
/// `player.canStartGame` is automatically set to false, because table is not full
|
||||||
let table = WaitingTable(oldTable: oldTable, removing: player)
|
let table = WaitingTable(oldTable: oldTable, removing: player.name)
|
||||||
tables[table.id] = table
|
tables[table.id] = table
|
||||||
table.sendUpdateToAllPlayers()
|
table.sendUpdateToAllPlayers()
|
||||||
writeTableToDisk(table: table)
|
player.$table.id = nil
|
||||||
|
// TODO: Update points for all players
|
||||||
|
return player.update(on: database)
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect(player: PlayerName, using socket: WebSocket) -> Bool {
|
func connect(player: PlayerName, using socket: WebSocket) -> Bool {
|
||||||
@ -208,7 +149,7 @@ final class TableManagement: DiskWriter {
|
|||||||
tables[newTable.id] = newTable
|
tables[newTable.id] = newTable
|
||||||
newTable.sendUpdateToAllPlayers()
|
newTable.sendUpdateToAllPlayers()
|
||||||
if newTable is FinishedTable || newTable is DealingTable {
|
if newTable is FinishedTable || newTable is DealingTable {
|
||||||
writeTableToDisk(table: newTable)
|
// TODO: Save new table
|
||||||
}
|
}
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
@ -232,6 +173,7 @@ final class TableManagement: DiskWriter {
|
|||||||
}
|
}
|
||||||
tables[newTable.id] = newTable
|
tables[newTable.id] = newTable
|
||||||
newTable.sendUpdateToAllPlayers()
|
newTable.sendUpdateToAllPlayers()
|
||||||
|
// TODO: Save new table
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,6 +191,7 @@ final class TableManagement: DiskWriter {
|
|||||||
}
|
}
|
||||||
tables[newTable.id] = newTable
|
tables[newTable.id] = newTable
|
||||||
newTable.sendUpdateToAllPlayers()
|
newTable.sendUpdateToAllPlayers()
|
||||||
|
// TODO: Save new table
|
||||||
return .success
|
return .success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,30 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import WebSocketKit
|
import WebSocketKit
|
||||||
|
|
||||||
|
typealias PlayerName = String
|
||||||
|
|
||||||
class Player {
|
class Player {
|
||||||
|
|
||||||
let name: PlayerName
|
let name: PlayerName
|
||||||
|
|
||||||
|
let totalPoints: Int
|
||||||
|
|
||||||
var socket: WebSocket?
|
var socket: WebSocket?
|
||||||
|
|
||||||
var isNextActor: Bool
|
var isNextActor: Bool
|
||||||
|
|
||||||
init(name: PlayerName, socket: WebSocket? = nil) {
|
init(name: PlayerName, points: Int, socket: WebSocket? = nil) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.socket = socket
|
self.socket = socket
|
||||||
self.isNextActor = false
|
self.isNextActor = false
|
||||||
|
self.totalPoints = points
|
||||||
}
|
}
|
||||||
|
|
||||||
init(player: Player) {
|
init(player: Player) {
|
||||||
self.name = player.name
|
self.name = player.name
|
||||||
self.socket = player.socket
|
self.socket = player.socket
|
||||||
self.isNextActor = false
|
self.isNextActor = false
|
||||||
|
self.totalPoints = player.totalPoints
|
||||||
}
|
}
|
||||||
|
|
||||||
var actions: [PlayerAction] {
|
var actions: [PlayerAction] {
|
||||||
@ -31,6 +37,7 @@ class Player {
|
|||||||
|
|
||||||
var info: PlayerInfo {
|
var info: PlayerInfo {
|
||||||
var result = PlayerInfo(name: name)
|
var result = PlayerInfo(name: name)
|
||||||
|
result.points = totalPoints
|
||||||
result.isConnected = isConnected
|
result.isConnected = isConnected
|
||||||
result.isNextActor = isNextActor
|
result.isNextActor = isNextActor
|
||||||
result.state = states.map { $0.rawValue }
|
result.state = states.map { $0.rawValue }
|
||||||
|
@ -7,7 +7,7 @@ let numberOfCardsPerPlayer = 8
|
|||||||
class AbstractTable<TablePlayer> where TablePlayer: Player {
|
class AbstractTable<TablePlayer> where TablePlayer: Player {
|
||||||
|
|
||||||
/// The unique id of the table
|
/// The unique id of the table
|
||||||
let id: TableId
|
let id: UUID
|
||||||
|
|
||||||
/// The name of the table
|
/// The name of the table
|
||||||
let name: TableName
|
let name: TableName
|
||||||
@ -28,6 +28,10 @@ class AbstractTable<TablePlayer> where TablePlayer: Player {
|
|||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var leavePenalty: Int {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
init(table: ManageableTable, players: [TablePlayer]) {
|
init(table: ManageableTable, players: [TablePlayer]) {
|
||||||
self.id = table.id
|
self.id = table.id
|
||||||
self.name = table.name
|
self.name = table.name
|
||||||
@ -35,7 +39,7 @@ class AbstractTable<TablePlayer> where TablePlayer: Player {
|
|||||||
self.players = players
|
self.players = players
|
||||||
}
|
}
|
||||||
|
|
||||||
init(id: TableId, name: TableName, isPublic: Bool, players: [TablePlayer]) {
|
init(id: UUID, name: TableName, isPublic: Bool, players: [TablePlayer]) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.isPublic = isPublic
|
self.isPublic = isPublic
|
||||||
@ -67,7 +71,7 @@ class AbstractTable<TablePlayer> where TablePlayer: Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func tableInfo(forPlayerAt index: Int) -> TableInfo {
|
func tableInfo(forPlayerAt index: Int) -> TableInfo {
|
||||||
var info = TableInfo(id: id, name: name)
|
var info = TableInfo(id: id.uuidString, name: name)
|
||||||
info.player = playerInfo(forIndex: index)!
|
info.player = playerInfo(forIndex: index)!
|
||||||
info.playerLeft = playerInfo(forIndex: (index + 1) % 4)
|
info.playerLeft = playerInfo(forIndex: (index + 1) % 4)
|
||||||
info.playerAcross = playerInfo(forIndex: (index + 2) % 4)
|
info.playerAcross = playerInfo(forIndex: (index + 2) % 4)
|
||||||
@ -85,7 +89,7 @@ class AbstractTable<TablePlayer> where TablePlayer: Player {
|
|||||||
extension AbstractTable: ManageableTable {
|
extension AbstractTable: ManageableTable {
|
||||||
|
|
||||||
var publicInfo: PublicTableInfo {
|
var publicInfo: PublicTableInfo {
|
||||||
.init(id: id, name: name, players: playerNames)
|
.init(id: id.uuidString, name: name, players: playerNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerNames: [PlayerName] {
|
var playerNames: [PlayerName] {
|
||||||
|
@ -4,7 +4,7 @@ import WebSocketKit
|
|||||||
protocol ManageableTable {
|
protocol ManageableTable {
|
||||||
|
|
||||||
/// The unique id of the table
|
/// The unique id of the table
|
||||||
var id: TableId { get }
|
var id: UUID { get }
|
||||||
|
|
||||||
/// The name of the table
|
/// The name of the table
|
||||||
var name: TableName { get }
|
var name: TableName { get }
|
||||||
@ -12,6 +12,8 @@ protocol ManageableTable {
|
|||||||
/// The table is visible in the list of tables and can be joined by anyone
|
/// The table is visible in the list of tables and can be joined by anyone
|
||||||
var isPublic: Bool { get }
|
var isPublic: Bool { get }
|
||||||
|
|
||||||
|
var leavePenalty: Int { get }
|
||||||
|
|
||||||
var playerNames: [PlayerName] { get }
|
var playerNames: [PlayerName] { get }
|
||||||
|
|
||||||
var allPlayers: [Player] { get }
|
var allPlayers: [Player] { get }
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Fluent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Represents a table where players are still joining and leaving.
|
Represents a table where players are still joining and leaving.
|
||||||
@ -10,8 +11,8 @@ final class WaitingTable: AbstractTable<WaitingPlayer> {
|
|||||||
players.count >= maximumPlayersPerTable
|
players.count >= maximumPlayersPerTable
|
||||||
}
|
}
|
||||||
|
|
||||||
init(id: TableId, name: TableName, isPublic: Bool, players: [PlayerName]) {
|
init(id: UUID, name: TableName, isPublic: Bool, players: [User]) {
|
||||||
let players = players.map { WaitingPlayer(name: $0) }
|
let players = players.map { WaitingPlayer(name: $0.name, points: $0.points) }
|
||||||
players.first!.isNextActor = true
|
players.first!.isNextActor = true
|
||||||
super.init(id: id, name: name, isPublic: isPublic, players: players)
|
super.init(id: id, name: name, isPublic: isPublic, players: players)
|
||||||
if isFull {
|
if isFull {
|
||||||
@ -24,10 +25,11 @@ final class WaitingTable: AbstractTable<WaitingPlayer> {
|
|||||||
- Parameter name: The name of the table
|
- Parameter name: The name of the table
|
||||||
- Parameter isPublic: The table is visible and joinable by everyone
|
- Parameter isPublic: The table is visible and joinable by everyone
|
||||||
*/
|
*/
|
||||||
init(newTable name: TableName, isPublic: Bool, creator: PlayerName) {
|
init(newTable object: Table) {
|
||||||
let player = WaitingPlayer(name: creator)
|
let user = object.players[0]
|
||||||
|
let player = WaitingPlayer(name: user.name, points: user.points)
|
||||||
player.isNextActor = true
|
player.isNextActor = true
|
||||||
super.init(id: .newToken(), name: name, isPublic: isPublic, players: [player])
|
super.init(id: object.id!, name: object.name, isPublic: object.isPublic, players: [player])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,6 +40,7 @@ final class WaitingTable: AbstractTable<WaitingPlayer> {
|
|||||||
- Parameter player: The name of the player to remove from the table.
|
- Parameter player: The name of the player to remove from the table.
|
||||||
*/
|
*/
|
||||||
init(oldTable: ManageableTable, removing player: PlayerName) {
|
init(oldTable: ManageableTable, removing player: PlayerName) {
|
||||||
|
// TODO: End game and distribute points
|
||||||
let players = oldTable.allPlayers
|
let players = oldTable.allPlayers
|
||||||
.filter {
|
.filter {
|
||||||
guard $0.name == player else {
|
guard $0.name == player else {
|
||||||
@ -46,7 +49,7 @@ final class WaitingTable: AbstractTable<WaitingPlayer> {
|
|||||||
_ = $0.disconnect()
|
_ = $0.disconnect()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
.map { WaitingPlayer(name: $0.name, socket: $0.socket) }
|
.map { WaitingPlayer(name: $0.name, points: $0.totalPoints, socket: $0.socket) }
|
||||||
players.first!.isNextActor = true
|
players.first!.isNextActor = true
|
||||||
super.init(table: oldTable, players: players)
|
super.init(table: oldTable, players: players)
|
||||||
}
|
}
|
||||||
@ -60,7 +63,7 @@ final class WaitingTable: AbstractTable<WaitingPlayer> {
|
|||||||
init(oldTableAdvancedByOne table: ManageableTable) {
|
init(oldTableAdvancedByOne table: ManageableTable) {
|
||||||
let players = table.allPlayers
|
let players = table.allPlayers
|
||||||
.rotatedByOne()
|
.rotatedByOne()
|
||||||
.map { WaitingPlayer(name: $0.name, socket: $0.socket) }
|
.map(WaitingPlayer.init)
|
||||||
super.init(table: table, players: players)
|
super.init(table: table, players: players)
|
||||||
players.forEach { $0.canStartGame = true }
|
players.forEach { $0.canStartGame = true }
|
||||||
players.first!.isNextActor = true
|
players.first!.isNextActor = true
|
||||||
@ -71,11 +74,11 @@ final class WaitingTable: AbstractTable<WaitingPlayer> {
|
|||||||
- Parameter player: The name of the player to add
|
- Parameter player: The name of the player to add
|
||||||
- Returns: `true`, if the player could be added, `false` if the table is full
|
- Returns: `true`, if the player could be added, `false` if the table is full
|
||||||
*/
|
*/
|
||||||
func add(player: PlayerName) -> Bool {
|
func add(player: PlayerName, points: Int) -> Bool {
|
||||||
guard !isFull else {
|
guard !isFull else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let player = WaitingPlayer(name: player)
|
let player = WaitingPlayer(name: player, points: points)
|
||||||
players.append(player)
|
players.append(player)
|
||||||
// Allow dealing of cards if table is full
|
// Allow dealing of cards if table is full
|
||||||
if isFull {
|
if isFull {
|
||||||
|
@ -1,20 +1,36 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
|
import Fluent
|
||||||
|
|
||||||
var database: Database!
|
var server: SQLiteDatabase!
|
||||||
|
|
||||||
// configures your application
|
// configures your application
|
||||||
public func configure(_ app: Application) throws {
|
public func configure(_ app: Application) throws {
|
||||||
|
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
||||||
|
|
||||||
// Set target environment
|
// Set target environment
|
||||||
app.environment = .production
|
app.environment = .production
|
||||||
app.logger.logLevel = .info // .notice
|
|
||||||
|
if app.environment == .development {
|
||||||
|
app.logger.logLevel = .info
|
||||||
|
print("[DEVELOPMENT] Using in-memory database")
|
||||||
|
app.databases.use(.sqlite(.memory), as: .sqlite)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
app.logger.logLevel = .notice
|
||||||
|
let dbFile = storageFolder.appendingPathComponent("db.sqlite").path
|
||||||
|
print("[PRODUCTION] Using database at \(dbFile)")
|
||||||
|
app.databases.use(.sqlite(.file(dbFile)), as: .sqlite)
|
||||||
|
}
|
||||||
|
app.migrations.add(UserTableMigration())
|
||||||
|
|
||||||
|
try app.autoMigrate().wait()
|
||||||
|
|
||||||
// serve files from /Public folder
|
// serve files from /Public folder
|
||||||
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||||
|
|
||||||
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
let db = app.databases.database(.sqlite, logger: .init(label: "Init"), on: app.databases.eventLoopGroup.next())!
|
||||||
database = try Database(storageFolder: storageFolder)
|
server = try SQLiteDatabase(db: db)
|
||||||
|
|
||||||
// register routes
|
// register routes
|
||||||
try routes(app)
|
try routes(app)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ func routes(_ app: Application) throws {
|
|||||||
- 424: The password could not be hashed
|
- 424: The password could not be hashed
|
||||||
- Returns: The session token for the registered user
|
- Returns: The session token for the registered user
|
||||||
*/
|
*/
|
||||||
app.post("player", "register", ":name") { req -> String in
|
app.post("player", "register", ":name") { req -> EventLoopFuture<SessionToken> in
|
||||||
guard let name = req.parameters.get("name"),
|
guard let name = req.parameters.get("name"),
|
||||||
let password = req.body.string else {
|
let password = req.body.string else {
|
||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
@ -42,10 +42,8 @@ func routes(_ app: Application) throws {
|
|||||||
guard let hash = try? req.password.hash(password) else {
|
guard let hash = try? req.password.hash(password) else {
|
||||||
throw Abort(.failedDependency) // 424
|
throw Abort(.failedDependency) // 424
|
||||||
}
|
}
|
||||||
guard let token = database.registerPlayer(named: name, hash: hash) else {
|
// Can throw conflict (409)
|
||||||
throw Abort(.conflict) // 409
|
return server.registerPlayer(named: name, hash: hash, in: req.db)
|
||||||
}
|
|
||||||
return token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,22 +56,17 @@ func routes(_ app: Application) throws {
|
|||||||
- 424: The password could not be hashed
|
- 424: The password could not be hashed
|
||||||
- Returns: Nothing
|
- Returns: Nothing
|
||||||
*/
|
*/
|
||||||
app.post("player", "delete", ":name") { req -> String in
|
app.post("player", "delete", ":name") { req -> EventLoopFuture<String> in
|
||||||
guard let name = req.parameters.get("name"),
|
guard let name = req.parameters.get("name"),
|
||||||
let password = req.body.string else {
|
let password = req.body.string else {
|
||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
guard let hash = database.passwordHashForExistingPlayer(named: name) else {
|
return server.passwordHashForExistingPlayer(named: name, in: req.db)
|
||||||
throw Abort(.forbidden) // 403
|
.guard({ hash in
|
||||||
}
|
(try? req.password.verify(password, created: hash)) ?? false
|
||||||
guard let isValid = try? req.password.verify(password, created: hash) else {
|
}, else: Abort(.forbidden)).flatMap { _ in
|
||||||
throw Abort(.failedDependency) // 424
|
server.deletePlayer(named: name, in: req.db)
|
||||||
}
|
}.map { "" }
|
||||||
guard isValid else {
|
|
||||||
throw Abort(.forbidden) // 403
|
|
||||||
}
|
|
||||||
database.deletePlayer(named: name)
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,22 +79,17 @@ func routes(_ app: Application) throws {
|
|||||||
- 424: The password could not be hashed
|
- 424: The password could not be hashed
|
||||||
- Returns: The session token for the user
|
- Returns: The session token for the user
|
||||||
*/
|
*/
|
||||||
app.post("player", "login", ":name") { req -> String in
|
app.post("player", "login", ":name") { req -> EventLoopFuture<String> in
|
||||||
guard let name = req.parameters.get("name"),
|
guard let name = req.parameters.get("name"),
|
||||||
let password = req.body.string else {
|
let password = req.body.string else {
|
||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
guard let hash = database.passwordHashForExistingPlayer(named: name) else {
|
return server.passwordHashForExistingPlayer(named: name, in: req.db)
|
||||||
throw Abort(.forbidden) // 403
|
.guard({ hash in
|
||||||
}
|
(try? req.password.verify(password, created: hash)) ?? false
|
||||||
guard let isValid = try? req.password.verify(password, created: hash) else {
|
}, else: Abort(.forbidden)).map { _ in
|
||||||
throw Abort(.failedDependency) // 424
|
server.startNewSessionForRegisteredPlayer(named: name)
|
||||||
}
|
}
|
||||||
guard isValid else {
|
|
||||||
throw Abort(.forbidden) // 403
|
|
||||||
}
|
|
||||||
let token = database.startNewSessionForRegisteredPlayer(named: name)
|
|
||||||
return token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -116,7 +104,7 @@ func routes(_ app: Application) throws {
|
|||||||
guard let token = req.body.string else {
|
guard let token = req.body.string else {
|
||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
|
guard let player = server.registeredPlayerExists(withSessionToken: token) else {
|
||||||
throw Abort(.unauthorized) // 401
|
throw Abort(.unauthorized) // 401
|
||||||
}
|
}
|
||||||
return player
|
return player
|
||||||
@ -135,7 +123,7 @@ func routes(_ app: Application) throws {
|
|||||||
guard let token = req.body.string else {
|
guard let token = req.body.string else {
|
||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
database.endSession(forSessionToken: token)
|
server.endSession(forSessionToken: token)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,10 +139,10 @@ func routes(_ app: Application) throws {
|
|||||||
guard let token = req.body.string else {
|
guard let token = req.body.string else {
|
||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
|
guard let player = server.registeredPlayerExists(withSessionToken: token) else {
|
||||||
throw Abort(.unauthorized) // 401
|
throw Abort(.unauthorized) // 401
|
||||||
}
|
}
|
||||||
guard let info = database.currentTableOfPlayer(named: player) else {
|
guard let info = server.currentTableOfPlayer(named: player) else {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return try encodeJSON(info)
|
return try encodeJSON(info)
|
||||||
@ -167,7 +155,7 @@ func routes(_ app: Application) throws {
|
|||||||
*/
|
*/
|
||||||
app.webSocket("session", "start") { req, socket in
|
app.webSocket("session", "start") { req, socket in
|
||||||
socket.onText { socket, text in
|
socket.onText { socket, text in
|
||||||
guard database.startSession(socket: socket, sessionToken: text) else {
|
guard server.startSession(socket: socket, sessionToken: text) else {
|
||||||
_ = socket.close()
|
_ = socket.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -185,7 +173,7 @@ func routes(_ app: Application) throws {
|
|||||||
- 400: Missing token, table name or invalid visibility
|
- 400: Missing token, table name or invalid visibility
|
||||||
- 401: The session token is invalid
|
- 401: The session token is invalid
|
||||||
*/
|
*/
|
||||||
app.post("table", "create", ":visibility", ":name") { req -> String in
|
app.post("table", "create", ":visibility", ":name") { req -> EventLoopFuture<String> in
|
||||||
guard let visibility = req.parameters.get("visibility"),
|
guard let visibility = req.parameters.get("visibility"),
|
||||||
let tableName = req.parameters.get("name"),
|
let tableName = req.parameters.get("name"),
|
||||||
let token = req.body.string else {
|
let token = req.body.string else {
|
||||||
@ -200,11 +188,11 @@ func routes(_ app: Application) throws {
|
|||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
|
guard let player = server.registeredPlayerExists(withSessionToken: token) else {
|
||||||
throw Abort(.unauthorized) // 401
|
throw Abort(.unauthorized) // 401
|
||||||
}
|
}
|
||||||
let table = database.createTable(named: tableName, player: player, isPublic: isPublic)
|
return server.createTable(named: tableName, player: player, isPublic: isPublic, in: req.db)
|
||||||
return try encodeJSON(table)
|
.flatMapThrowing(encodeJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -219,10 +207,10 @@ func routes(_ app: Application) throws {
|
|||||||
guard let token = req.body.string else {
|
guard let token = req.body.string else {
|
||||||
throw Abort(.badRequest) // 400
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
guard database.isValid(sessionToken: token) else {
|
guard server.isValid(sessionToken: token) else {
|
||||||
throw Abort(.forbidden) // 403
|
throw Abort(.forbidden) // 403
|
||||||
}
|
}
|
||||||
let list = database.getPublicTableInfos()
|
let list = server.getPublicTableInfos()
|
||||||
return try encodeJSON(list)
|
return try encodeJSON(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,26 +226,14 @@ func routes(_ app: Application) throws {
|
|||||||
- 417: The table is already full and can't be joined
|
- 417: The table is already full and can't be joined
|
||||||
- Returns: Nothing
|
- Returns: Nothing
|
||||||
*/
|
*/
|
||||||
app.post("table", "join", ":table") { req -> String in
|
app.post("table", "join", ":table") { req -> EventLoopFuture<String> in
|
||||||
guard let table = req.parameters.get("table"),
|
guard let string = req.parameters.get("table"),
|
||||||
|
let table = UUID(uuidString: string),
|
||||||
let token = req.body.string else {
|
let token = req.body.string else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest)
|
||||||
}
|
}
|
||||||
switch database.join(tableId: table, playerToken: token) {
|
return server.join(tableId: table, playerToken: token, in: req.db)
|
||||||
case .success(let table):
|
.flatMapThrowing(encodeJSON)
|
||||||
return try encodeJSON(table)
|
|
||||||
case .failure(let result):
|
|
||||||
switch result {
|
|
||||||
case .invalidToken:
|
|
||||||
throw Abort(.unauthorized) // 401
|
|
||||||
case .alreadyJoinedOtherTable:
|
|
||||||
throw Abort(.forbidden) // 403
|
|
||||||
case .tableNotFound:
|
|
||||||
throw Abort(.gone) // 410
|
|
||||||
case .tableIsFull:
|
|
||||||
throw Abort(.expectationFailed) // 417
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -268,14 +244,11 @@ func routes(_ app: Application) throws {
|
|||||||
- 401: The session token is invalid
|
- 401: The session token is invalid
|
||||||
- Returns: Nothing
|
- Returns: Nothing
|
||||||
*/
|
*/
|
||||||
app.post("table", "leave") { req -> String in
|
app.post("table", "leave") { req -> EventLoopFuture<String> in
|
||||||
guard let token = req.body.string else {
|
guard let token = req.body.string else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest)
|
||||||
}
|
}
|
||||||
guard database.leaveTable(playerToken: token) else {
|
return server.leaveTable(playerToken: token, in: req.db).map { "" }
|
||||||
throw Abort(.unauthorized) // 401
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post("player", "action", ":action") { req -> String in
|
app.post("player", "action", ":action") { req -> String in
|
||||||
@ -285,9 +258,9 @@ func routes(_ app: Application) throws {
|
|||||||
}
|
}
|
||||||
let result: PlayerActionResult
|
let result: PlayerActionResult
|
||||||
if let action = PlayerAction(rawValue: actionString) {
|
if let action = PlayerAction(rawValue: actionString) {
|
||||||
result = database.performAction(playerToken: token, action: action)
|
result = server.performAction(playerToken: token, action: action)
|
||||||
} else if let game = GameType(rawValue: actionString) {
|
} else if let game = GameType(rawValue: actionString) {
|
||||||
result = database.select(game: game, playerToken: token)
|
result = server.select(game: game, playerToken: token)
|
||||||
} else {
|
} else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest)
|
||||||
}
|
}
|
||||||
@ -313,7 +286,7 @@ func routes(_ app: Application) throws {
|
|||||||
let card = Card(id: cardId) else {
|
let card = Card(id: cardId) else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest)
|
||||||
}
|
}
|
||||||
switch database.play(card: card, playerToken: token) {
|
switch server.play(card: card, playerToken: token) {
|
||||||
case .success:
|
case .success:
|
||||||
return ""
|
return ""
|
||||||
case .invalidToken:
|
case .invalidToken:
|
||||||
|
Loading…
Reference in New Issue
Block a user