2021-12-01 22:47:19 +01:00
|
|
|
import Foundation
|
|
|
|
import WebSocketKit
|
|
|
|
import Vapor
|
2021-12-22 22:13:09 +01:00
|
|
|
import Fluent
|
2023-02-01 16:44:07 +01:00
|
|
|
import Clairvoyant
|
2021-12-01 22:47:19 +01:00
|
|
|
|
|
|
|
let maximumPlayersPerTable = 4
|
|
|
|
|
|
|
|
typealias TableId = String
|
|
|
|
typealias TableName = String
|
|
|
|
|
2021-12-22 22:13:09 +01:00
|
|
|
final class TableManagement {
|
2021-12-01 22:47:19 +01:00
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/// All tables indexed by their id
|
2021-12-22 22:13:09 +01:00
|
|
|
private var tables = [UUID : ManageableTable]()
|
2023-02-01 16:44:07 +01:00
|
|
|
|
|
|
|
/// The metric to log the current number of tables
|
|
|
|
private var tableCountMetric: Metric<Int>
|
|
|
|
|
|
|
|
/// The metric describing the number of players currently sitting at a table
|
|
|
|
private let playingPlayerCountMetric: Metric<Int>
|
|
|
|
|
|
|
|
/// The metric describing the number of players currently connected via a websocket
|
|
|
|
private let connectedPlayerCountMetric: Metric<Int>
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/**
|
|
|
|
Load the tables from a file in the storage folder
|
|
|
|
- Throws: Errors when the file could not be read
|
|
|
|
*/
|
2023-02-16 18:31:30 +01:00
|
|
|
init(database: Database) async throws {
|
|
|
|
self.tableCountMetric = try await .init(
|
2023-02-06 11:25:30 +01:00
|
|
|
"schafkopf.tables",
|
|
|
|
name: "Open tables",
|
|
|
|
description: "The number of currently available tables")
|
2023-02-16 18:31:30 +01:00
|
|
|
self.playingPlayerCountMetric = try await .init(
|
2023-02-06 11:25:30 +01:00
|
|
|
"schafkopf.playing",
|
|
|
|
name: "Sitting players",
|
|
|
|
description: "The number of players currently sitting at a table")
|
2023-02-16 18:31:30 +01:00
|
|
|
self.connectedPlayerCountMetric = try await .init(
|
2023-02-06 11:25:30 +01:00
|
|
|
"schafkopf.connected",
|
|
|
|
name: "Connected players",
|
|
|
|
description: "The number of players with a websocket connection to the server")
|
2023-02-01 16:44:07 +01:00
|
|
|
|
2023-02-16 18:31:30 +01:00
|
|
|
|
|
|
|
do {
|
|
|
|
try await loadTables(from: database)
|
|
|
|
} catch {
|
|
|
|
log("Failed to load tables: \(error)")
|
2023-02-01 16:44:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func loadTables(from database: Database) async throws {
|
|
|
|
try await Table.query(on: database).with(\.$players).all().forEach { table in
|
|
|
|
guard !table.players.isEmpty else {
|
|
|
|
_ = table.delete(on: database)
|
|
|
|
return
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2023-02-01 16:44:07 +01:00
|
|
|
let id = table.id!
|
|
|
|
self.tables[id] = WaitingTable(id: id, name: table.name, isPublic: table.isPublic, players: table.players)
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2023-02-06 22:03:02 +01:00
|
|
|
log("\(tables.count) tables loaded")
|
2023-02-16 18:31:30 +01:00
|
|
|
await logTableCount()
|
|
|
|
await logPlayingPlayerCount()
|
|
|
|
await logConnectedPlayerCount()
|
2023-02-01 16:44:07 +01:00
|
|
|
}
|
|
|
|
|
2023-02-16 18:31:30 +01:00
|
|
|
private func logTableCount() async {
|
2023-09-07 13:56:27 +02:00
|
|
|
_ = try? await tableCountMetric.update(tables.count)
|
2023-02-01 16:44:07 +01:00
|
|
|
}
|
|
|
|
|
2023-02-16 18:31:30 +01:00
|
|
|
private func logPlayingPlayerCount() async {
|
2023-02-01 16:44:07 +01:00
|
|
|
let count = tables.values.sum { $0.playerCount }
|
2023-09-07 13:56:27 +02:00
|
|
|
_ = try? await playingPlayerCountMetric.update(count)
|
2023-02-01 16:44:07 +01:00
|
|
|
}
|
|
|
|
|
2023-02-16 18:31:30 +01:00
|
|
|
private func logConnectedPlayerCount() async {
|
2023-02-01 16:44:07 +01:00
|
|
|
let count = tables.values.sum { $0.numberOfConnectedPlayers }
|
2023-09-07 13:56:27 +02:00
|
|
|
_ = try? await connectedPlayerCountMetric.update(count)
|
2021-12-21 15:50:49 +01:00
|
|
|
}
|
|
|
|
|
2021-12-01 22:47:19 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2022-10-12 19:28:28 +02:00
|
|
|
func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) async throws -> TableInfo {
|
2021-12-22 22:13:09 +01:00
|
|
|
let table = Table(name: name, isPublic: isPublic)
|
2022-10-12 19:28:28 +02:00
|
|
|
try await table.create(on: database)
|
|
|
|
player.$table.id = table.id
|
|
|
|
try await player.update(on: database)
|
2022-10-12 22:02:01 +02:00
|
|
|
let waitingTable = WaitingTable(newTable: table, user: player)
|
2022-10-12 19:28:28 +02:00
|
|
|
self.tables[waitingTable.id] = waitingTable
|
2023-02-16 18:31:30 +01:00
|
|
|
await logTableCount()
|
|
|
|
await logPlayingPlayerCount()
|
2022-10-12 19:28:28 +02:00
|
|
|
return waitingTable.tableInfo(forPlayer: player.name)
|
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
|
|
|
|
*/
|
2022-10-12 19:28:28 +02:00
|
|
|
func join(tableId: UUID, player: User, in database: Database) async throws -> TableInfo {
|
|
|
|
let table = try joinableTable(for: player, id: tableId)
|
|
|
|
player.$table.id = table.id
|
|
|
|
try await player.update(on: database)
|
|
|
|
table.sendUpdateToAllPlayers()
|
2023-02-16 18:31:30 +01:00
|
|
|
await logPlayingPlayerCount()
|
2022-10-12 19:28:28 +02:00
|
|
|
return table.tableInfo(forPlayer: player.name)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func joinableTable(for player: User, id tableId: UUID) throws -> ManageableTable {
|
|
|
|
if let existing = self.currentTable(for: player.name) {
|
|
|
|
guard existing.id == tableId else {
|
|
|
|
throw Abort(.forbidden) // 403
|
2021-12-22 22:13:09 +01:00
|
|
|
}
|
2022-10-12 19:28:28 +02:00
|
|
|
return existing
|
|
|
|
}
|
|
|
|
guard let table = self.tables[tableId] else {
|
|
|
|
throw Abort(.gone) // 410
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2022-10-12 19:28:28 +02:00
|
|
|
guard let joinableTable = table as? WaitingTable,
|
|
|
|
joinableTable.add(player: player.name, points: player.points) else {
|
|
|
|
throw Abort(.expectationFailed) // 417
|
|
|
|
}
|
|
|
|
return joinableTable
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
/**
|
|
|
|
A player leaves the table it previously joined
|
2021-12-22 22:13:09 +01:00
|
|
|
- Parameter player: The player leaving the table
|
2021-12-03 18:03:29 +01:00
|
|
|
*/
|
2022-10-12 19:28:28 +02:00
|
|
|
func leaveTable(player: User, in database: Database) async throws {
|
2021-12-22 22:13:09 +01:00
|
|
|
guard let oldTable = currentTable(for: player.name) else {
|
2022-10-12 19:28:28 +02:00
|
|
|
return
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|
2021-12-23 11:16:29 +01:00
|
|
|
player.$table.id = nil
|
|
|
|
guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else {
|
|
|
|
tables[oldTable.id] = nil
|
2023-02-16 18:31:30 +01:00
|
|
|
await logTableCount()
|
2022-10-12 19:28:28 +02:00
|
|
|
try await player.update(on: database)
|
|
|
|
try await Table.query(on: database).filter(\.$id == oldTable.id).delete()
|
|
|
|
return
|
2021-12-23 11:16:29 +01:00
|
|
|
}
|
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
|
|
|
tables[table.id] = table
|
2022-10-18 11:40:08 +02:00
|
|
|
#warning("Update points for all players, add penalty if running game")
|
2021-12-09 11:11:17 +01:00
|
|
|
table.sendUpdateToAllPlayers()
|
2023-02-16 18:31:30 +01:00
|
|
|
await logPlayingPlayerCount()
|
|
|
|
await logConnectedPlayerCount()
|
2022-10-12 19:28:28 +02:00
|
|
|
try await player.update(on: database)
|
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
|
|
|
|
}
|
2023-02-16 18:31:30 +01:00
|
|
|
let result = table.connect(player: player, using: socket)
|
|
|
|
Task {
|
|
|
|
await logConnectedPlayerCount()
|
|
|
|
}
|
|
|
|
return result
|
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)
|
2023-02-16 18:31:30 +01:00
|
|
|
Task {
|
|
|
|
await logConnectedPlayerCount()
|
|
|
|
}
|
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 {
|
2023-02-06 22:03:02 +01:00
|
|
|
log("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 {
|
2021-12-22 22:13:09 +01:00
|
|
|
// TODO: Save new table
|
2021-12-21 15:50:49 +01:00
|
|
|
}
|
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 {
|
2023-02-06 22:03:02 +01:00
|
|
|
log("Player \(player) wants to play \(game.rawValue), but no table joined")
|
2021-12-06 18:28:35 +01:00
|
|
|
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 {
|
2023-02-06 22:03:02 +01:00
|
|
|
log("Game selected by \(player), but no playing table \(table.name) created")
|
2021-12-09 11:11:17 +01:00
|
|
|
table.sendUpdateToAllPlayers()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
tables[newTable.id] = newTable
|
|
|
|
newTable.sendUpdateToAllPlayers()
|
2021-12-22 22:13:09 +01:00
|
|
|
// TODO: Save new table
|
2021-12-09 11:11:17 +01:00
|
|
|
return .success
|
2021-12-06 18:28:35 +01:00
|
|
|
}
|
|
|
|
|
2022-10-18 11:40:08 +02:00
|
|
|
func play(card: Card, player: PlayerName, in database: Database) async throws -> 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
|
2022-10-18 11:40:08 +02:00
|
|
|
if let finished = newTable as? FinishedTable {
|
|
|
|
try await finished.updatePlayerPoints(in: database)
|
|
|
|
}
|
2021-12-09 11:11:17 +01:00
|
|
|
newTable.sendUpdateToAllPlayers()
|
|
|
|
return .success
|
2021-12-06 11:43:30 +01:00
|
|
|
}
|
2022-01-24 17:15:11 +01:00
|
|
|
|
|
|
|
func disconnectAllSockets() {
|
|
|
|
tables.values.forEach { $0.disconnectAllPlayers() }
|
2023-02-16 18:31:30 +01:00
|
|
|
Task {
|
|
|
|
await logConnectedPlayerCount()
|
|
|
|
}
|
2022-01-24 17:15:11 +01:00
|
|
|
}
|
2021-12-01 22:47:19 +01:00
|
|
|
}
|