2021-12-01 22:47:19 +01:00
|
|
|
import Foundation
|
|
|
|
import WebSocketKit
|
|
|
|
import Vapor
|
|
|
|
|
|
|
|
let maximumPlayersPerTable = 4
|
|
|
|
|
|
|
|
typealias TableId = String
|
|
|
|
typealias TableName = String
|
|
|
|
|
|
|
|
final class TableManagement: DiskWriter {
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/// All tables indexed by their id
|
2021-12-18 15:08:43 +01:00
|
|
|
private var tables = [TableId : ManageableTable]()
|
2021-12-01 22:47:19 +01:00
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/// The handle to the file where the tables are persisted
|
2021-12-21 15:50:49 +01:00
|
|
|
var storageFile: FileHandle
|
2021-12-01 22:47:19 +01:00
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/// The url to the file where the tables are persisted
|
2021-12-01 22:47:19 +01:00
|
|
|
let storageFileUrl: URL
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-01 22:47:19 +01:00
|
|
|
init(storageFolder: URL) throws {
|
|
|
|
let url = storageFolder.appendingPathComponent("tables.txt")
|
|
|
|
|
|
|
|
storageFileUrl = url
|
|
|
|
storageFile = try Self.prepareFile(at: url)
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
var entries = [TableId : (name: TableName, isPublic: Bool, players: [PlayerName])]()
|
2021-12-21 15:50:49 +01:00
|
|
|
var redundantEntries = 0
|
2021-12-01 22:47:19 +01:00
|
|
|
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
|
2021-12-21 15:50:49 +01:00
|
|
|
redundantEntries += 2 // One for creation, one for deletion
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if entries[id] != nil {
|
|
|
|
redundantEntries += 1
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-21 15:50:49 +01:00
|
|
|
entries[id] = (name, isPublic, players)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
entries.forEach { id, tableData in
|
2021-12-18 15:08:43 +01:00
|
|
|
tables[id] = WaitingTable(id: id, name: tableData.name, isPublic: tableData.isPublic, players: tableData.players)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-21 15:50:49 +01:00
|
|
|
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: ":")
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-01 22:47:19 +01:00
|
|
|
@discardableResult
|
2021-12-18 15:08:43 +01:00
|
|
|
private func writeTableToDisk(table: ManageableTable) -> Bool {
|
2021-12-21 15:50:49 +01:00
|
|
|
let entry = entry(for: table)
|
2021-12-01 22:47:19 +01:00
|
|
|
return writeToDisk(line: entry)
|
|
|
|
}
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-01 22:47:19 +01:00
|
|
|
@discardableResult
|
2021-12-03 18:03:29 +01:00
|
|
|
private func writeTableDeletionEntry(tableId: TableId) -> Bool {
|
2021-12-09 11:11:17 +01:00
|
|
|
let entry = [tableId, "", "", ""]
|
|
|
|
.joined(separator: ":")
|
2021-12-01 22:47:19 +01:00
|
|
|
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
|
2021-12-03 18:03:29 +01:00
|
|
|
- Parameter isPublic: Indicates that this is a game joinable by everyone
|
2021-12-01 22:47:19 +01:00
|
|
|
- Returns: The table id
|
|
|
|
*/
|
2021-12-06 11:43:30 +01:00
|
|
|
func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableInfo {
|
2021-12-18 15:08:43 +01:00
|
|
|
let table = WaitingTable(newTable: name, isPublic: isPublic, creator: player)
|
2021-12-03 18:03:29 +01:00
|
|
|
tables[table.id] = table
|
|
|
|
writeTableToDisk(table: table)
|
2021-12-09 11:11:17 +01:00
|
|
|
return table.tableInfo(forPlayer: player)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/// A list of all public tables
|
|
|
|
var publicTableList: [PublicTableInfo] {
|
|
|
|
tables.values.filter { $0.isPublic }.map { $0.publicInfo }
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/**
|
|
|
|
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? {
|
2021-12-09 11:11:17 +01:00
|
|
|
currentTable(for: player)?.tableInfo(forPlayer: player)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
|
2021-12-18 15:08:43 +01:00
|
|
|
private func currentTable(for player: PlayerName) -> ManageableTable? {
|
|
|
|
tables.values.first(where: { $0.playerNames.contains(player) })
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Join a table.
|
2021-12-03 18:03:29 +01:00
|
|
|
- Parameter tableId: The table to join
|
|
|
|
- Parameter player: The name of the player who wants to join.
|
2021-12-01 22:47:19 +01:00
|
|
|
- Returns: The result of the join operation
|
|
|
|
*/
|
2021-12-03 18:03:29 +01:00
|
|
|
func join(tableId: TableId, player: PlayerName) -> Result<TableInfo, JoinTableResult> {
|
|
|
|
if let existing = currentTable(for: player) {
|
|
|
|
guard existing.id == tableId else {
|
|
|
|
return .failure(.alreadyJoinedOtherTable)
|
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
return .success(existing.tableInfo(forPlayer: player))
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
guard let table = tables[tableId] else {
|
|
|
|
return .failure(.tableNotFound)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
guard let joinableTable = table as? WaitingTable else {
|
|
|
|
return .failure(.tableIsFull)
|
|
|
|
}
|
|
|
|
guard joinableTable.add(player: player) else {
|
2021-12-03 18:03:29 +01:00
|
|
|
return .failure(.tableIsFull)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
writeTableToDisk(table: table)
|
2021-12-09 11:11:17 +01:00
|
|
|
joinableTable.sendUpdateToAllPlayers()
|
|
|
|
return .success(joinableTable.tableInfo(forPlayer: player))
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/**
|
|
|
|
A player leaves the table it previously joined
|
|
|
|
- Parameter player: The name of the player
|
|
|
|
*/
|
|
|
|
func leaveTable(player: PlayerName) {
|
2021-12-09 11:11:17 +01:00
|
|
|
guard let oldTable = currentTable(for: player) else {
|
2021-12-01 22:47:19 +01:00
|
|
|
return
|
|
|
|
}
|
2021-12-18 15:08:43 +01:00
|
|
|
/// `player.canStartGame` is automatically set to false, because table is not full
|
2021-12-09 11:11:17 +01:00
|
|
|
let table = WaitingTable(oldTable: oldTable, removing: player)
|
|
|
|
tables[table.id] = table
|
|
|
|
table.sendUpdateToAllPlayers()
|
2021-12-03 18:03:29 +01:00
|
|
|
writeTableToDisk(table: table)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func connect(player: PlayerName, using socket: WebSocket) -> Bool {
|
2021-12-03 18:03:29 +01:00
|
|
|
guard let table = currentTable(for: player) else {
|
2021-12-01 22:47:19 +01:00
|
|
|
return false
|
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
return table.connect(player: player, using: socket)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func disconnect(player: PlayerName) {
|
2021-12-03 18:03:29 +01:00
|
|
|
guard let table = currentTable(for: player) else {
|
2021-12-01 22:47:19 +01:00
|
|
|
return
|
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
table.disconnect(player: player)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 11:11:17 +01:00
|
|
|
func performAction(player: PlayerName, action: PlayerAction) -> PlayerActionResult {
|
2021-12-03 18:03:29 +01:00
|
|
|
guard let table = currentTable(for: player) else {
|
2021-12-09 11:18:26 +01:00
|
|
|
print("Player \(player) wants to \(action.id), but no table joined")
|
2021-12-01 22:47:19 +01:00
|
|
|
return .noTableJoined
|
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
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()
|
2021-12-21 15:50:49 +01:00
|
|
|
if newTable is FinishedTable || newTable is DealingTable {
|
|
|
|
writeTableToDisk(table: newTable)
|
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
return .success
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-06 11:43:30 +01:00
|
|
|
|
2021-12-06 18:28:35 +01:00
|
|
|
func select(game: GameType, player: PlayerName) -> PlayerActionResult {
|
2021-12-09 11:11:17 +01:00
|
|
|
guard let aTable = currentTable(for: player) else {
|
2021-12-06 18:28:35 +01:00
|
|
|
print("Player \(player) wants to play \(game.rawValue), but no table joined")
|
|
|
|
return .noTableJoined
|
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
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
|
2021-12-06 18:28:35 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 11:11:17 +01:00
|
|
|
func play(card: Card, player: PlayerName) -> PlayerActionResult {
|
2021-12-06 11:43:30 +01:00
|
|
|
guard let table = currentTable(for: player) else {
|
|
|
|
return .noTableJoined
|
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
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
|
2021-12-06 11:43:30 +01:00
|
|
|
}
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|