Skip to content

Instantly share code, notes, and snippets.

@spllr
Last active November 12, 2017 15:18
Show Gist options
  • Save spllr/719039a5095513e3961ce6fff3d1267b to your computer and use it in GitHub Desktop.
Save spllr/719039a5095513e3961ce6fff3d1267b to your computer and use it in GitHub Desktop.
A simple game of Tic Tac Toe in swift
// 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