253 lines
8.2 KiB
Swift
253 lines
8.2 KiB
Swift
|
import Foundation
|
||
|
import WebSocketKit
|
||
|
import Vapor
|
||
|
|
||
|
let maximumPlayersPerTable = 4
|
||
|
|
||
|
typealias TableId = String
|
||
|
typealias TableName = String
|
||
|
|
||
|
final class TableManagement: DiskWriter {
|
||
|
|
||
|
/// A list of table ids for public games
|
||
|
private var publicTables = Set<TableId>()
|
||
|
|
||
|
/// A mapping from table id to table name (for all tables)
|
||
|
private var tableNames = [TableId: TableName]()
|
||
|
|
||
|
/// A mapping from table id to participating players
|
||
|
private var tablePlayers = [TableId: [PlayerName]]()
|
||
|
|
||
|
/// A reverse list of players and their table id
|
||
|
private var playerTables = [PlayerName: TableId]()
|
||
|
|
||
|
private var playerConnections = [PlayerName : WebSocket]()
|
||
|
|
||
|
private var tablePhase = [TableId: GamePhase]()
|
||
|
|
||
|
|
||
|
let storageFile: FileHandle
|
||
|
|
||
|
let storageFileUrl: URL
|
||
|
|
||
|
init(storageFolder: URL) throws {
|
||
|
let url = storageFolder.appendingPathComponent("tables.txt")
|
||
|
|
||
|
storageFileUrl = url
|
||
|
storageFile = try Self.prepareFile(at: url)
|
||
|
|
||
|
var entries = [TableId : (name: TableName, public: Bool, players: [PlayerName])]()
|
||
|
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
|
||
|
} else {
|
||
|
entries[id] = (name, isPublic, players)
|
||
|
}
|
||
|
}
|
||
|
entries.forEach { id, table in
|
||
|
tableNames[id] = table.name
|
||
|
if table.public {
|
||
|
publicTables.insert(id)
|
||
|
}
|
||
|
tablePlayers[id] = table.players
|
||
|
tablePhase[id] = .waitingForPlayers
|
||
|
for player in table.players {
|
||
|
playerTables[player] = id
|
||
|
}
|
||
|
}
|
||
|
print("Loaded \(tableNames.count) tables")
|
||
|
}
|
||
|
|
||
|
@discardableResult
|
||
|
private func save(table tableId: TableId) -> Bool {
|
||
|
let name = tableNames[tableId]!
|
||
|
let visible = publicTables.contains(tableId) ? "public" : "private"
|
||
|
let players = tablePlayers[tableId]!
|
||
|
let entry = [tableId, name, visible, players.joined(separator: ",")].joined(separator: ":")
|
||
|
return writeToDisk(line: entry)
|
||
|
}
|
||
|
|
||
|
@discardableResult
|
||
|
private func deleteTable(tableId: TableId) -> Bool {
|
||
|
let entry = [tableId, "", "", ""].joined(separator: ":")
|
||
|
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
|
||
|
- Parameter visible: Indicates that this is a game joinable by everyone
|
||
|
- Returns: The table id
|
||
|
*/
|
||
|
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId {
|
||
|
let tableId = TableId.newToken()
|
||
|
|
||
|
tableNames[tableId] = name
|
||
|
tablePlayers[tableId] = [player]
|
||
|
playerTables[player] = tableId
|
||
|
|
||
|
if visible {
|
||
|
publicTables.insert(tableId)
|
||
|
}
|
||
|
save(table: tableId)
|
||
|
return tableId
|
||
|
}
|
||
|
|
||
|
func getPublicTableInfos() -> [TableInfo] {
|
||
|
publicTables.map(tableInfo).sorted()
|
||
|
}
|
||
|
|
||
|
private func tableInfo(id tableId: TableId) -> TableInfo {
|
||
|
let players = tablePlayers[tableId]!.map(playerState)
|
||
|
return TableInfo(
|
||
|
id: tableId,
|
||
|
name: tableNames[tableId]!,
|
||
|
players: players,
|
||
|
tableIsFull: players.count == maximumPlayersPerTable)
|
||
|
}
|
||
|
|
||
|
private func playerState(_ player: PlayerName) -> TableInfo.PlayerState {
|
||
|
.init(name: player, connected: playerIsConnected(player))
|
||
|
}
|
||
|
|
||
|
private func playerIsConnected(_ player: PlayerName) -> Bool {
|
||
|
playerConnections[player] != nil
|
||
|
}
|
||
|
|
||
|
func currentTableOfPlayer(named player: PlayerName) -> TableId? {
|
||
|
playerTables[player]
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Join a table.
|
||
|
- Returns: The result of the join operation
|
||
|
*/
|
||
|
func join(tableId: TableId, player: PlayerName) -> JoinTableResult {
|
||
|
guard var players = tablePlayers[tableId] else {
|
||
|
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)
|
||
|
}
|
||
|
tablePlayers[tableId] = players
|
||
|
playerTables[player] = tableId
|
||
|
save(table: tableId)
|
||
|
return .success
|
||
|
}
|
||
|
|
||
|
func remove(player: PlayerName, fromTable tableId: TableId) {
|
||
|
tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player }
|
||
|
disconnect(player: player)
|
||
|
playerTables[player] = nil
|
||
|
// TODO: End game if needed
|
||
|
// TODO: Remove table if empty
|
||
|
save(table: tableId)
|
||
|
}
|
||
|
|
||
|
func remove(player: PlayerName) {
|
||
|
guard let tableId = playerTables[player] else {
|
||
|
return
|
||
|
}
|
||
|
// Already saves table to disk
|
||
|
remove(player: player, fromTable: tableId)
|
||
|
}
|
||
|
|
||
|
func connect(player: PlayerName, using socket: WebSocket) -> Bool {
|
||
|
guard let tableId = playerTables[player] else {
|
||
|
return false
|
||
|
}
|
||
|
guard let players = tablePlayers[tableId] else {
|
||
|
print("Player \(player) was assigned to missing table \(tableId.prefix(5))")
|
||
|
playerTables[player] = nil
|
||
|
return false
|
||
|
}
|
||
|
guard players.contains(player) else {
|
||
|
print("Player \(player) wants updates for table \(tableId.prefix(5)) it didn't join")
|
||
|
return false
|
||
|
}
|
||
|
playerConnections[player] = socket
|
||
|
sendTableInfo(toTable: tableId)
|
||
|
// TODO: Send cards to player
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func disconnect(player: PlayerName) {
|
||
|
if let socket = playerConnections.removeValue(forKey: player) {
|
||
|
if !socket.isClosed {
|
||
|
_ = socket.close()
|
||
|
}
|
||
|
}
|
||
|
guard let tableId = playerTables[player] else {
|
||
|
return
|
||
|
}
|
||
|
sendTableInfo(toTable: tableId)
|
||
|
// Change table phase to waiting
|
||
|
}
|
||
|
|
||
|
private func sendTableInfo(toTable tableId: TableId) {
|
||
|
let name = tableNames[tableId]!
|
||
|
var players = tablePlayers[tableId]!
|
||
|
let isFull = players.count == maximumPlayersPerTable
|
||
|
for _ in players.count..<maximumPlayersPerTable {
|
||
|
players.append("")
|
||
|
}
|
||
|
let states = players.map(playerState)
|
||
|
players.enumerated().forEach { index, player in
|
||
|
guard let socket = playerConnections[player] else {
|
||
|
return
|
||
|
}
|
||
|
let info = TableInfo(
|
||
|
id: tableId,
|
||
|
name: name,
|
||
|
players: states.rotated(toStartAt: index),
|
||
|
tableIsFull: isFull)
|
||
|
socket.send(info)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func dealCards(player: PlayerName) -> DealCardResult {
|
||
|
guard let tableId = playerTables[player] else {
|
||
|
return .noTableJoined
|
||
|
}
|
||
|
guard let players = tablePlayers[tableId] else {
|
||
|
playerTables[player] = nil
|
||
|
print("Player \(player) assigned to missing table \(tableId.prefix(5))")
|
||
|
return .noTableJoined
|
||
|
}
|
||
|
guard players.count == maximumPlayersPerTable else {
|
||
|
return .tableNotFull
|
||
|
}
|
||
|
|
||
|
let cards = Dealer.deal()
|
||
|
let handCards = ["", "", "", ""]
|
||
|
players.enumerated().forEach { index, player in
|
||
|
guard let socket = playerConnections[player] else {
|
||
|
return
|
||
|
}
|
||
|
let info = CardInfo(
|
||
|
cards: cards[index].map { .init(card: $0.id, playable: false) },
|
||
|
tableCards: handCards.rotated(toStartAt: index))
|
||
|
socket.send(info)
|
||
|
}
|
||
|
return .success
|
||
|
}
|
||
|
}
|