Sync push
This commit is contained in:
@ -1,32 +0,0 @@
|
||||
import Foundation
|
||||
import WebSocketKit
|
||||
|
||||
private let encoder = JSONEncoder()
|
||||
|
||||
enum ClientMessageType: String {
|
||||
|
||||
/// The names and connection states of th player, plus table name and id
|
||||
case tableInfo = "t"
|
||||
|
||||
/// The hand cards of the player and the cards on the table
|
||||
case cardInfo = "c"
|
||||
|
||||
/// The game is in the bidding phase
|
||||
case biddingInfo = "b"
|
||||
|
||||
///
|
||||
}
|
||||
|
||||
protocol ClientMessage: Encodable {
|
||||
|
||||
static var type: ClientMessageType { get }
|
||||
}
|
||||
|
||||
extension WebSocket {
|
||||
|
||||
func send<T>(_ data: T) where T: ClientMessage {
|
||||
let json = try! encoder.encode(data)
|
||||
let string = String(data: json, encoding: .utf8)!
|
||||
self.send(T.type.rawValue + string)
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ final class Database {
|
||||
|
||||
func deletePlayer(named name: PlayerName) {
|
||||
_ = players.deletePlayer(named: name)
|
||||
tables.remove(player: name)
|
||||
tables.leaveTable(player: name)
|
||||
}
|
||||
|
||||
func isValid(sessionToken token: SessionToken) -> Bool {
|
||||
@ -63,8 +63,8 @@ final class Database {
|
||||
players.registeredPlayerExists(withSessionToken: token)
|
||||
}
|
||||
|
||||
func currentTableOfPlayer(named player: PlayerName) -> TableId {
|
||||
tables.currentTableOfPlayer(named: player) ?? ""
|
||||
func currentTableOfPlayer(named player: PlayerName) -> TableInfo? {
|
||||
tables.tableInfo(player: player)
|
||||
}
|
||||
|
||||
// MARK: Tables
|
||||
@ -73,20 +73,20 @@ final class Database {
|
||||
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
|
||||
- Parameter isPublic: Indicates that this is a game joinable by everyone
|
||||
- Returns: The table id
|
||||
*/
|
||||
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId {
|
||||
tables.createTable(named: name, player: player, visible: visible)
|
||||
func createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId {
|
||||
tables.createTable(named: name, player: player, isPublic: isPublic)
|
||||
}
|
||||
|
||||
func getPublicTableInfos() -> [TableInfo] {
|
||||
tables.getPublicTableInfos()
|
||||
func getPublicTableInfos() -> [PublicTableInfo] {
|
||||
tables.publicTableList
|
||||
}
|
||||
|
||||
func join(tableId: TableId, playerToken: SessionToken) -> JoinTableResult {
|
||||
func join(tableId: TableId, playerToken: SessionToken) -> Result<TableInfo,JoinTableResult> {
|
||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
||||
return .invalidToken
|
||||
return .failure(.invalidToken)
|
||||
}
|
||||
return tables.join(tableId: tableId, player: player)
|
||||
}
|
||||
@ -95,14 +95,14 @@ final class Database {
|
||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
||||
return false
|
||||
}
|
||||
tables.remove(player: player)
|
||||
tables.leaveTable(player: player)
|
||||
return true
|
||||
}
|
||||
|
||||
func dealCards(playerToken: SessionToken) -> DealCardResult {
|
||||
func performAction(playerToken: SessionToken, action: Player.Action) -> PlayerActionResult {
|
||||
guard let player = players.registeredPlayerExists(withSessionToken: playerToken) else {
|
||||
return .invalidToken
|
||||
}
|
||||
return tables.dealCards(player: player)
|
||||
return tables.performAction(player: player, action: action)
|
||||
}
|
||||
}
|
||||
|
@ -40,19 +40,23 @@ extension DiskWriter {
|
||||
}
|
||||
}
|
||||
|
||||
func readLinesFromDisk() throws -> [String] {
|
||||
func readDataFromDisk() throws -> Data {
|
||||
if #available(macOS 10.15.4, *) {
|
||||
guard let data = try storageFile.readToEnd() else {
|
||||
try storageFile.seekToEnd()
|
||||
return []
|
||||
return Data()
|
||||
}
|
||||
return parseLines(data: data)
|
||||
return data
|
||||
} else {
|
||||
let data = storageFile.readDataToEndOfFile()
|
||||
return parseLines(data: data)
|
||||
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")
|
||||
|
@ -9,34 +9,27 @@ 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]()
|
||||
|
||||
/// All tables indexed by their id
|
||||
private var tables = [TableId : Table]()
|
||||
|
||||
/// The handle to the file where the tables are persisted
|
||||
let 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
|
||||
- Parameter storageFolder: The url to the folder where the table file is stored
|
||||
- Throws: Errors when the file could not be read
|
||||
*/
|
||||
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])]()
|
||||
var entries = [TableId : (name: TableName, isPublic: Bool, players: [PlayerName])]()
|
||||
try readLinesFromDisk().forEach { line in
|
||||
// Each line has parts: ID | NAME | PLAYER, PLAYER, ...
|
||||
let parts = line.components(separatedBy: ":")
|
||||
@ -54,31 +47,38 @@ final class TableManagement: DiskWriter {
|
||||
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
|
||||
}
|
||||
entries.forEach { id, tableData in
|
||||
let table = Table(id: id, name: tableData.name, isPublic: tableData.isPublic)
|
||||
tableData.players.forEach { _ = table.add(player: $0) }
|
||||
tables[id] = table
|
||||
}
|
||||
print("Loaded \(tableNames.count) tables")
|
||||
print("Loaded \(tables.count) tables")
|
||||
}
|
||||
|
||||
/**
|
||||
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 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: ":")
|
||||
private func writeTableToDisk(table: Table) -> Bool {
|
||||
let visible = table.isPublic ? "public" : "private"
|
||||
let players = table.playerNames.joined(separator: ",")
|
||||
let entry = [table.id, table.name, visible, players].joined(separator: ":")
|
||||
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 deleteTable(tableId: TableId) -> Bool {
|
||||
private func writeTableDeletionEntry(tableId: TableId) -> Bool {
|
||||
let entry = [tableId, "", "", ""].joined(separator: ":")
|
||||
return writeToDisk(line: entry)
|
||||
}
|
||||
@ -87,166 +87,88 @@ final class TableManagement: DiskWriter {
|
||||
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
|
||||
- Parameter isPublic: 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 createTable(named name: TableName, player: PlayerName, isPublic: Bool) -> TableId {
|
||||
let table = Table(newTable: name, isPublic: isPublic)
|
||||
_ = table.add(player: name)
|
||||
tables[table.id] = table
|
||||
writeTableToDisk(table: table)
|
||||
return table.id
|
||||
}
|
||||
|
||||
func getPublicTableInfos() -> [TableInfo] {
|
||||
publicTables.map(tableInfo).sorted()
|
||||
/// A list of all public tables
|
||||
var publicTableList: [PublicTableInfo] {
|
||||
tables.values.filter { $0.isPublic }.map { $0.publicInfo }
|
||||
}
|
||||
|
||||
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)
|
||||
/**
|
||||
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? {
|
||||
currentTable(for: player)?.compileInfo(for: player)
|
||||
}
|
||||
|
||||
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]
|
||||
private func currentTable(for player: PlayerName) -> Table? {
|
||||
tables.values.first(where: { $0.contains(player: player) })
|
||||
}
|
||||
|
||||
/**
|
||||
Join a table.
|
||||
- Parameter tableId: The table to join
|
||||
- Parameter player: The name of the player who wants to join.
|
||||
- Returns: The result of the join operation
|
||||
*/
|
||||
func join(tableId: TableId, player: PlayerName) -> JoinTableResult {
|
||||
guard var players = tablePlayers[tableId] else {
|
||||
return .tableNotFound
|
||||
func join(tableId: TableId, player: PlayerName) -> Result<TableInfo, JoinTableResult> {
|
||||
if let existing = currentTable(for: player) {
|
||||
guard existing.id == tableId else {
|
||||
return .failure(.alreadyJoinedOtherTable)
|
||||
}
|
||||
return .success(existing.compileInfo(for: player)!)
|
||||
}
|
||||
guard !players.contains(player) else {
|
||||
return .success
|
||||
guard let table = tables[tableId] else {
|
||||
return .failure(.tableNotFound)
|
||||
}
|
||||
guard players.count < maximumPlayersPerTable else {
|
||||
return .tableIsFull
|
||||
guard table.add(player: player) else {
|
||||
return .failure(.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
|
||||
writeTableToDisk(table: table)
|
||||
return .success(table.compileInfo(for: player)!)
|
||||
}
|
||||
|
||||
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 {
|
||||
/**
|
||||
A player leaves the table it previously joined
|
||||
- Parameter player: The name of the player
|
||||
*/
|
||||
func leaveTable(player: PlayerName) {
|
||||
guard let table = currentTable(for: player) else {
|
||||
return
|
||||
}
|
||||
// Already saves table to disk
|
||||
remove(player: player, fromTable: tableId)
|
||||
table.remove(player: player)
|
||||
writeTableToDisk(table: table)
|
||||
}
|
||||
|
||||
func connect(player: PlayerName, using socket: WebSocket) -> Bool {
|
||||
guard let tableId = playerTables[player] else {
|
||||
guard let table = currentTable(for: 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
|
||||
return table.connect(player: player, using: socket)
|
||||
}
|
||||
|
||||
func disconnect(player: PlayerName) {
|
||||
if let socket = playerConnections.removeValue(forKey: player) {
|
||||
if !socket.isClosed {
|
||||
_ = socket.close()
|
||||
}
|
||||
}
|
||||
guard let tableId = playerTables[player] else {
|
||||
guard let table = currentTable(for: player) else {
|
||||
return
|
||||
}
|
||||
sendTableInfo(toTable: tableId)
|
||||
// Change table phase to waiting
|
||||
table.disconnect(player: player)
|
||||
}
|
||||
|
||||
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 {
|
||||
func performAction(player: PlayerName, action: Player.Action) -> PlayerActionResult {
|
||||
guard let table = currentTable(for: 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
|
||||
return table.perform(action: action, forPlayer: player)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user