import Foundation #if os(Linux) import FoundationXML #endif /// The game handler is responsible for the communication with the game server /// and the selection of the game logic. class SCGameHandler: NSObject, XMLParserDelegate { // MARK: - Properties /// The TCP socket used for the communication with the game server. private let socket: SCSocket /// The reservation code to join a prepared game. private let reservation: String /// The strategy selected by the user. private let strategy: String /// The room id associated with the joined game. private var roomId: String! /// The player color of the delegate (game logic). private var playerColor: SCPlayerColor! /// The current state of the game. private var gameState: SCGameState! /// Indicates whether the game state has been initially created. private var gameStateCreated = false /// Indicates whether the game loop should be left. private var leaveGame = false /// The current player score which is parsed. private var score: SCScore! /// The scores of the players. private var scores = [SCScore]() /// The winner of the game. private var winner: SCWinner? /// Indicates whether the game result has been received. private var gameResultReceived = false /// The field that is currently processed. private var field: SCField! /// The type of the last move. private var lastMoveType: SCMoveType! /// The piece used by the last move. private var lastMovePiece: SCPiece! /// The coordinates of the last move. private var lastMoveCoords = [SCCubeCoordinate]() /// The characters found by the parser. private var foundChars = "" /// The delegate (game logic) which handles the requests of the game server. var delegate: SCGameHandlerDelegate? // MARK: - Initializers /// Creates a new game handler with the given TCP socket, the given /// reservation code and the given strategy. /// /// The TCP socket must already be connected before using this initializer. /// /// - Parameters: /// - socket: The socket used for the communication with the game server. /// - reservation: The reservation code to join a prepared game. /// - strategy: The selected strategy. init(socket: SCSocket, reservation: String, strategy: String) { self.socket = socket self.reservation = reservation self.strategy = strategy } // MARK: - Methods /// Handles the game. func handleGame() { if self.reservation.isEmpty { // Join a game. self.socket.send(message: #""#) } else { // Join a prepared game. self.socket.send(message: #""#) } // The root element for the received XML document. A temporary fix for // the XMLParser. guard let rootElem = "".data(using: .utf8) else { return } // Loop until the game is over. while !self.leaveGame { // Receive the message from the game server. var data = Data() self.socket.receive(into: &data) // Parse the received XML document. let parser = XMLParser(data: rootElem + data) parser.delegate = self _ = parser.parse() } } /// Exits the game with the given error message. /// /// - Parameter error: The error message to print into the standard output. private func exitGame(withError error: String = "") { if !error.isEmpty { print("ERROR: \(error)") } self.leaveGame = true } // MARK: - XMLParserDelegate func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { // Reset the found characters. self.foundChars = "" switch elementName { case "data": // Check whether a class attribute exists. guard let classAttr = attributeDict["class"] else { parser.abortParsing() self.exitGame(withError: "The class attribute of the data element is missing!") break } switch classAttr { case "result": self.gameResultReceived = true case "sc.framework.plugins.protocol.MoveRequest": guard var move = self.delegate?.onMoveRequested() else { parser.abortParsing() self.exitGame(withError: "No move has been sent!") break } // Send the move returned by the game logic to the game // server. var mv = "" switch move.type { case .dragMove: let start = move.start! let dest = move.destination! mv += #""# case .setMove: let piece = move.piece! let dest = move.destination! mv += #""# default: break } mv += move.debugHints.reduce(into: "") { $0 += #""# } let moveType = "\(move.type)".lowercased() self.socket.send(message: #"\#(mv)"#) case "welcomeMessage": guard let colorAttr = attributeDict["color"], let color = SCPlayerColor(rawValue: colorAttr.uppercased()) else { parser.abortParsing() self.exitGame(withError: "The player color of the welcome message is missing or could not be parsed!") break } // Save the player color of this game client. self.playerColor = color default: break } case "destination", "start": guard let xAttr = attributeDict["x"], let x = Int(xAttr), let yAttr = attributeDict["y"], let y = Int(yAttr) else { parser.abortParsing() self.exitGame(withError: "The start or destination coordinate could not be parsed!") break } // Save the start or destination coordinate of the last move. self.lastMoveCoords.append(SCCubeCoordinate(x: x, y: y)) case "field": if !self.gameStateCreated { guard let xAttr = attributeDict["x"], let x = Int(xAttr), let yAttr = attributeDict["y"], let y = Int(yAttr), let obstructedAttr = attributeDict["isObstructed"], let obstructed = Bool(obstructedAttr) else { parser.abortParsing() self.exitGame(withError: "The field could not be parsed!") break } // Save the field into a temporary variable. self.field = SCField(coordinate: SCCubeCoordinate(x: x, y: y), obstructed: obstructed) } case "joined": guard let roomId = attributeDict["roomId"] else { parser.abortParsing() self.exitGame(withError: "The room ID is missing!") break } // Save the room id of the game. self.roomId = roomId case "lastMove": guard let classAttr = attributeDict["class"], let type = SCMoveType(rawValue: classAttr.uppercased()) else { parser.abortParsing() self.exitGame(withError: "The last move could not be parsed!") break } // Save the type of the last move. self.lastMoveType = type case "left": // Leave the game. parser.abortParsing() self.delegate?.onGameEnded() self.exitGame() case "piece": if !self.gameStateCreated || self.lastMoveType != nil { guard let ownerAttr = attributeDict["owner"], let owner = SCPlayerColor(rawValue: ownerAttr), let typeAttr = attributeDict["type"], let type = SCPieceType(rawValue: typeAttr) else { parser.abortParsing() self.exitGame(withError: "The piece could not be parsed!") break } // Create the piece. let piece = SCPiece(owner: owner, type: type) // Use the piece for a field or the last move. if self.lastMoveType != nil { self.lastMovePiece = piece } else { self.field.pieces.append(piece) } } case "score": guard let causeAttr = attributeDict["cause"], let cause = SCScoreCause(rawValue: causeAttr) else { parser.abortParsing() self.exitGame(withError: "The score could not be parsed!") break } // Create the score object. self.score = SCScore(cause: cause, reason: attributeDict["reason"]) case "state": if !self.gameStateCreated { guard let startPlayerAttr = attributeDict["startPlayerColor"], let startPlayer = SCPlayerColor(rawValue: startPlayerAttr) else { parser.abortParsing() self.exitGame(withError: "The initial game state could not be parsed!") break } // Create the initial game state. self.gameState = SCGameState(startPlayer: startPlayer) // TODO: Select the game logic based on the strategy. // Create the game logic. self.delegate = SCGameLogic(player: self.playerColor) } case "winner": guard let displayName = attributeDict["displayName"], let colorAttr = attributeDict["color"], let color = SCPlayerColor(rawValue: colorAttr) else { parser.abortParsing() self.exitGame(withError: "The winner could not be parsed!") break } // Save the winner of the game. self.winner = SCWinner(displayName: displayName, player: color) default: break } } func parser(_ parser: XMLParser, foundCharacters string: String) { self.foundChars += string } func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { switch elementName { case "data": if self.gameResultReceived { // Notify the delegate that the game result has been // received. self.delegate?.onGameResultReceived(SCGameResult(scores: self.scores, winner: self.winner)) } case "field": if !self.gameStateCreated { // Update the field on the game board. self.gameState.setField(field: self.field) } case "lastMove": // Create the last move. var lastMove = SCMove() switch self.lastMoveType { case .dragMove: lastMove = SCMove(start: self.lastMoveCoords.first!, destination: self.lastMoveCoords.last!) case .setMove: lastMove = SCMove(piece: self.lastMovePiece, destination: self.lastMoveCoords.last!) default: break } // Perform the last move on the game state. if !self.gameState.performMove(move: lastMove) { parser.abortParsing() self.exitGame(withError: "The last move could not be performed on the game state!") } // Destroy the last move. self.lastMoveType = nil self.lastMoveCoords.removeAll() case "part": // Add the found value to the current score object. self.score.values.append(self.foundChars) case "score": // Append the current score object to the array of scores. self.scores.append(self.score) case "state": self.gameStateCreated = true // Notify the delegate that the game state has been updated. self.delegate?.onGameStateUpdated(SCGameState(withGameState: self.gameState)) default: break } } }