Last active
November 12, 2017 15:18
-
-
Save spllr/719039a5095513e3961ce6fff3d1267b to your computer and use it in GitHub Desktop.
A simple game of Tic Tac Toe in swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Playground by @spllr. Use as you like. | |
import Foundation | |
/// A Game of Tic Tac Toe | |
public struct Game: CustomStringConvertible, Codable | |
{ | |
/// Game Errors | |
/// | |
/// - gameFinished: The game has finished and no plays are possible | |
/// - positionOccupied: The played positions is already taken by a player | |
/// - positionOutsideBoard: The position is outside the board | |
public enum PlayError: Error | |
{ | |
case gameFinished | |
case positionOccupied | |
case positionOutsideBoard | |
} | |
/// A `Player` represents a player in the Game | |
/// | |
/// - None: No `Player` | |
/// - X: The **X** player | |
/// - O: The **O** player | |
public enum Player: String, Codable | |
{ | |
case None = " " | |
case X | |
case O | |
} | |
/// A diagonal direction on the board | |
/// | |
/// - leftTopRightBottom: The diagonal starting at the left top, ending at he right bottom | |
/// - leftBottomRightTop: The diagonal starting at the left bottom, ending at the right top | |
public enum Diagonal | |
{ | |
case leftTopRightBottom | |
case leftBottomRightTop | |
} | |
/// The shared encoder for all Games | |
private static var jsonEncoder: JSONEncoder = { | |
let encoder = JSONEncoder() | |
encoder.outputFormatting = .prettyPrinted | |
return encoder | |
}() | |
/// The shared decoder for all Games | |
private static var jsonDecoder: JSONDecoder = { | |
let decoder = JSONDecoder() | |
return decoder | |
}() | |
/// Returns a Game from JSON data | |
/// | |
/// - Parameter data: JSON encoded Game data | |
/// - Returns: A Game if the game could be decoded, otherwise nil | |
public static func from(json data: Data) -> Game? | |
{ | |
do | |
{ | |
return try jsonDecoder.decode(self, from: data) | |
} | |
catch | |
{ | |
return nil | |
} | |
} | |
/// Returns a Game from a JSON string | |
/// | |
/// - Parameter data: JSON encoded Game string | |
/// - Returns: A Game if the game could be decoded, otherwise nil | |
public static func from(json string: String) -> Game? | |
{ | |
guard let data = string.data(using: .utf8) else | |
{ | |
return nil | |
} | |
return from(json: data) | |
} | |
// MARK: - Properties | |
/// All playable positions on in the Game | |
private var positions: [Player] | |
/// The current Player | |
public var currentPlayer: Player | |
{ | |
if numberOfPlays(by: .X) > numberOfPlays(by: .O) | |
{ | |
return .O | |
} | |
return .X | |
} | |
/// The maximal number of remaining plays | |
public var remainingPlays: Int | |
{ | |
return positions.filter { $0 == .None }.count | |
} | |
/// The winning `Player` of the Game. `Player.None` if there is no winner (yet) | |
public var winner: Player | |
{ | |
var winningPlayer: Player = .None | |
for index in (0..<boardSize) | |
{ | |
winningPlayer = winner(row: index) | |
if winningPlayer != .None | |
{ | |
return winningPlayer | |
} | |
winningPlayer = winner(column: index) | |
if winningPlayer != .None | |
{ | |
return winningPlayer | |
} | |
} | |
winningPlayer = winner(diagonal: .leftBottomRightTop) | |
if winningPlayer != .None | |
{ | |
return winningPlayer | |
} | |
return winner(diagonal: .leftTopRightBottom) | |
} | |
/// The size of the board. | |
/// | |
/// A game with `boardSize` `3` would have `9` playable field (`3x3`) | |
public var boardSize: Int | |
{ | |
return Int(sqrt(Double(positions.count))) | |
} | |
/// True is the Game is finished | |
public var finished: Bool | |
{ | |
return remainingPlays == 0 || winner != .None | |
} | |
/// The unique identifier of the game | |
public var identifier: UUID = UUID() | |
// MARK: - Initializers | |
/// Create a new Game | |
/// | |
/// - Parameter boardSize: The size of the board (boardSize`X`boardSize) | |
public init() | |
{ | |
positions = [Player](repeating: .None, count: 3 * 3) | |
} | |
// MARK: - CustomStringConvertible | |
/// The description string of the Game. Will plot the `Game` as ASCII | |
public var description: String | |
{ | |
return (0..<boardSize).map { (index) -> String in | |
return try! row(index).map { $0.rawValue }.joined(separator: "|") | |
}.joined(separator: "\n------\n") | |
} | |
// MARK: - API | |
// MARK: Internal | |
/// Checks if an index is inside the Game board | |
/// | |
/// - Parameter index: Index to check | |
/// - Returns: True if the index is inside the board | |
private func isInsideBoard(_ index: Int) -> Bool | |
{ | |
return index >= 0 && index < boardSize | |
} | |
/// Returns a winning `Player` given a list of positions | |
/// | |
/// - Parameter positions: List of `Player` positions | |
/// - Returns: The winning `Player`, .None if no `Player` has won | |
private func winner(in positions: [Player]) -> Player | |
{ | |
let players = Set(positions) | |
if let winner = players.first, players.count == 1 | |
{ | |
return winner | |
} | |
return .None | |
} | |
// MARK: Public | |
/// Play a position on the board | |
/// | |
/// - Parameters: | |
/// - row: The row | |
/// - column: The column | |
/// - Returns: The winner of the Game after the play. Game.Player.None if there is no winner yet | |
public mutating func play(row rowIndex: Int, column columnIndex: Int) throws -> Player | |
{ | |
guard finished == false else | |
{ | |
throw PlayError.gameFinished | |
} | |
guard isInsideBoard(rowIndex), isInsideBoard(columnIndex) else | |
{ | |
throw PlayError.positionOutsideBoard | |
} | |
guard self[rowIndex, columnIndex] == .None else | |
{ | |
throw PlayError.positionOccupied | |
} | |
self[rowIndex, columnIndex] = currentPlayer | |
print("\n\(self)\n") | |
return winner | |
} | |
/// Returns the `Players` in the row at index | |
/// | |
/// - Parameter index: The row index | |
/// - Returns: An Array with all the players in the row at index | |
/// - Throws: If the index is out of bounds | |
public func row(_ index: Int) throws -> [Player] | |
{ | |
guard isInsideBoard(index) else | |
{ | |
throw PlayError.positionOutsideBoard | |
} | |
let startIndex = index * boardSize | |
let endIndex = startIndex + boardSize | |
return Array(positions[(startIndex..<endIndex)]) | |
} | |
/// Returns the `Players` in the column at the index | |
/// | |
/// - Parameter index: The column index | |
/// - Returns: An Array with all the `Players` in the column at index | |
/// - Throws: If the index is out of bounds | |
public func column(_ index: Int) throws -> [Player] | |
{ | |
guard isInsideBoard(index) else | |
{ | |
throw PlayError.positionOutsideBoard | |
} | |
return (0..<boardSize).map { positions[index + $0 * boardSize] } | |
} | |
/// Returns the `Players` in the diagonal position | |
/// | |
/// - Parameter direction: The diagonal direction | |
/// - Returns: The list of players | |
public func diagonal(_ direction: Diagonal) -> [Player] | |
{ | |
switch direction | |
{ | |
case .leftTopRightBottom: | |
return (0..<boardSize).map { positions[$0 * boardSize + $0]} | |
case .leftBottomRightTop: | |
return (0..<boardSize).map { positions[$0 * boardSize + (boardSize - 1 - $0)]} | |
} | |
} | |
/// Returns the number of plays by the player | |
/// | |
/// - Parameter player: The Player | |
/// - Returns: Number of plays by the Player | |
public func numberOfPlays(by player: Player) -> Int | |
{ | |
return positions.filter { $0 == player }.count | |
} | |
/// Returns the winning `Player` in the row at index | |
/// | |
/// - Parameter rowIndex: The index of the row | |
/// - Returns: The winner `Player`. When the player is `.None`, there is no winner in the row | |
public func winner(row index: Int) -> Player | |
{ | |
do | |
{ | |
return winner(in: try row(index)) | |
} | |
catch | |
{ | |
return .None | |
} | |
} | |
/// Returns the winning `Player` in the column at index | |
/// | |
/// - Parameter rowIndex: The index of the column | |
/// - Returns: The winner `Player`. When the player is `.None`, there is no winner in the column | |
public func winner(column index: Int) -> Player | |
{ | |
do | |
{ | |
return winner(in: try column(index)) | |
} | |
catch | |
{ | |
return .None | |
} | |
} | |
/// Returns the winner in one of the diagonals | |
/// | |
/// - Parameter direction: The diagonaldirection | |
/// - Returns: The winner of the diagonal | |
public func winner(diagonal direction: Diagonal) -> Player | |
{ | |
return winner(in: diagonal(direction)) | |
} | |
/// Returns the `Game` as JSON Data. | |
/// | |
/// The JSON Data can be used to share the `Game` and play with your | |
/// your friends | |
/// | |
/// - Returns: The `Game` data as JSON Data | |
public func jsonData() -> Data | |
{ | |
do | |
{ | |
return try Game.jsonEncoder.encode(self) | |
} | |
catch | |
{ | |
return Data(bytes: []) | |
} | |
} | |
/// Returns the `Game` as a JSON String. | |
/// | |
/// The JSON String can be used to share the `Game` and play with your | |
/// your friends | |
/// | |
/// - Returns: The `Game` data as a JSON String | |
public func jsonString() -> String | |
{ | |
guard let string = String(data: jsonData(), encoding: .utf8) else | |
{ | |
return "{}" | |
} | |
return string | |
} | |
// MARK: - Subscripts | |
/// Set or get the `Player` at a position on the board | |
/// | |
/// - Parameters: | |
/// - rowIndex: The row | |
/// - columnIndex: The column | |
public private(set) subscript (rowIndex: Int, columnIndex: Int) -> Player | |
{ | |
get | |
{ | |
guard isInsideBoard(rowIndex), isInsideBoard(columnIndex) else | |
{ | |
return .None | |
} | |
do | |
{ | |
let players = try row(rowIndex) | |
return players[columnIndex] | |
} | |
catch | |
{ | |
return .None | |
} | |
} | |
set | |
{ | |
guard isInsideBoard(rowIndex), isInsideBoard(columnIndex) else | |
{ | |
return | |
} | |
let positionIndex = rowIndex * boardSize + columnIndex | |
positions[positionIndex] = newValue | |
} | |
} | |
} | |
/** | |
Lets play a game | |
*/ | |
var game = Game() | |
do | |
{ | |
try game.play(row: 1, column: 1) | |
try game.play(row: 0, column: 1) | |
try game.play(row: 0, column: 0) | |
try game.play(row: 1, column: 2) | |
try game.play(row: 2, column: 2) | |
} | |
catch Game.PlayError.gameFinished | |
{ | |
print("The game was already finished") | |
} | |
catch Game.PlayError.positionOccupied | |
{ | |
print("The position was already occupied") | |
} | |
catch Game.PlayError.positionOutsideBoard | |
{ | |
print("The position was outside the board") | |
} | |
/** | |
Share a game with your friend | |
*/ | |
var sharedGame = Game() | |
/// You go first | |
try sharedGame.play(row: 1, column: 1) | |
/// And send it to your friend | |
let gameJson = sharedGame.jsonString() | |
print(gameJson) | |
/// You friend can now continue the game | |
if var receivedGame = Game.from(json: gameJson) | |
{ | |
do | |
{ | |
try receivedGame.play(row: 0, column: 0) | |
} | |
catch Game.PlayError.gameFinished | |
{ | |
print("The game was already finished") | |
} | |
catch Game.PlayError.positionOccupied | |
{ | |
print("The position was already occupied") | |
} | |
catch Game.PlayError.positionOutsideBoard | |
{ | |
print("The position was outside the board") | |
} | |
print("New state for game \(receivedGame.identifier)\n") | |
print(receivedGame) | |
// And now your friend can send it back to you. | |
// Rinse and repeat | |
print(receivedGame.jsonString()) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment