Skip to content

Instantly share code, notes, and snippets.

@marciok
Last active August 11, 2016 15:42
Show Gist options
  • Select an option

  • Save marciok/b554e83fa0767dc8ef2d69c3d591c6e3 to your computer and use it in GitHub Desktop.

Select an option

Save marciok/b554e83fa0767dc8ef2d69c3d591c6e3 to your computer and use it in GitHub Desktop.
//
// A Tic-Tac-Toe game written in Swift using a functional programming approach.
//
//
// Created by Marcio Klepacz on 8/10/16.
//
//
import Foundation
struct Board {
/// The empty space of the board.
let emptySpace = " "
/// The board field.
let field: [[String]]
/// The current state of the board.
let currentState: State
/// The winner of the game, since Game doesn't have any state, the board has to know.
let winner: Piece?
/**
Initializes a new board with the provided field and state
- Paramerters:
- field: The board field
- currentState: The current state for the new board
- winner: The winner, if there's any
*/
init(field: [[String]], currentState: State, winner: Piece? = nil) {
self.currentState = currentState
self.field = field
self.winner = winner
}
/**
Initializes a new board with X play turn
*/
init() {
self.currentState = .Xplay
let row = [emptySpace, emptySpace, emptySpace]
self.field = [row, row, row]
self.winner = nil
}
/**
Piece to be placed on the board
- X: X player
- O: O player
*/
enum Piece: String {
case X
case O
}
/**
State to track the game
- Xplay: X turn to play
- Oplay: O turn to play
- Finished: Game is over
- InvalidMove: When there is already a piece on the requested position
*/
enum State {
case Xplay
case Oplay
case Finished
case InvalidMove
}
enum Row: Int {
case Top
case Mid
case Bottom
}
enum Column: Int {
case Left
case Mid
case Right
}
}
struct Game {
private static let winnerSequenceSize = 3
/**
Make a move on the game
- Parameter:
- board: The board game that you wish to play
- row: the row where your piece will be placed
- column: the column where your piece will be placed
*/
internal static func play(board: Board, row: Board.Row, column: Board.Column) -> Board {
if board.currentState == .Finished { return board }
return checkForEmptySpaces(checkForWinner(togglePlayer(addMove(board, row: row, column: column))))
}
internal static func botPlay(board: Board) -> Board {
// 1. get the empty spaces
var emptySpaces: [(Int,Int)] = []
for rowIndex in 0..<board.field.count {
let col = board.field[rowIndex]
for colIndex in 0..<col.count {
if col[colIndex] == board.emptySpace {
// 2. Put inside an array
emptySpaces.append((rowIndex, colIndex))
}
}
}
// 4. Create a position
let randNumber = Int(arc4random_uniform(UInt32(emptySpaces.count)))
let randPair = emptySpaces[randNumber]
let row = Board.Row(rawValue: randPair.0)
let col = Board.Column(rawValue: randPair.1)
return self.play(board, row: row!, column: col!)
}
private static func checkForEmptySpaces(board: Board) -> Board {
var hasSpace = false
for row in board.field {
hasSpace = row.filter { $0 == board.emptySpace }.count > 0
}
if !hasSpace {
return changeState(board, newState: .Finished)
}
return board
}
private static func checkForWinner(board: Board) -> Board {
let sequencesInRow = flattenRow(board.field)
if hasHorizontalWinnerSequence(sequencesInRow) {
return winner(board, sequence: sequencesInRow)
}
var winnerSequenceBuffer: [String] = []
let winnerSequence = winnerTuples(board.field).flatMap { (x,y) -> [String] in
winnerSequenceBuffer += [board.field[x][y]]
if winnerSequenceBuffer.count == winnerSequenceSize {
if hasUniquePiece(winnerSequenceBuffer) {
return winnerSequenceBuffer
} else {
winnerSequenceBuffer = []
}
}
return []
}
return winner(board, sequence: winnerSequence)
}
/**
Changes the game state
- Parameter:
- board: The board game that you want change the state
*/
static func changeState(board: Board, newState: Board.State) -> Board {
return Board(field: board.field, currentState: newState)
}
private static func changeField(board: Board, newField: [[String]]) -> Board {
return Board(field: newField, currentState: board.currentState)
}
private static func finishGame(board: Board, winner: Board.Piece) -> Board {
return Board(field: board.field, currentState: .Finished, winner: winner)
}
private static func addMove(board: Board, row: Board.Row, column: Board.Column) -> Board {
var field = board.field
let x = row.rawValue
let y = column.rawValue
if field[x][y] != board.emptySpace {
return changeState(board, newState: .InvalidMove)
}
if board.currentState == .Xplay {
field[x][y] = Board.Piece.X.rawValue
} else if board.currentState == .Oplay {
field[x][y] = Board.Piece.O.rawValue
}
return changeField(board, newField: field)
}
private static func togglePlayer(board: Board) -> Board {
if board.currentState == .Oplay {
return changeState(board, newState: .Xplay)
} else if board.currentState == .Xplay {
return changeState(board, newState: .Oplay)
}
return board
}
private static func winner(board: Board, sequence: [String]) -> Board {
guard let firstElement = sequence.first else { return board }
return finishGame(board, winner: Board.Piece(rawValue: firstElement)!)
}
// MARK: Helpers
private static func flattenRow(field: [[String]]) -> [String] {
return field.filter { return hasUniquePiece($0) }.flatMap { $0 }
}
private static func hasHorizontalWinnerSequence(sequence: [String]) -> Bool {
return sequence.count == winnerSequenceSize
}
private static func winnerTuples(filed: [[String]]) -> [(Int, Int)] {
var diagonalTuples: [(Int, Int)] = Array()
var verticalTuples: [(Int, Int)] = Array()
for rowIndex in 0..<board.field.count {
diagonalTuples += [(rowIndex, rowIndex)]
for columnIndex in 0..<board.field.first!.count {
verticalTuples += [(columnIndex, rowIndex)]
}
}
return verticalTuples + diagonalTuples + diagonalTuples.reverse()
}
private static func hasUniquePiece(row: [String]) -> Bool {
let uniqueElements = Set(row)
return uniqueElements.count == 1 && uniqueElements.first != board.emptySpace
}
}
/**
Print a board and its current status
- Parameter board: board to be printed
*/
func print(board: Board) {
if let winner = board.winner {
print("The winner is: \(winner) 🎉")
}
for row in board.field {
print(row)
}
print("---------------")
}
// MARK: UI
var board = Board()
var previousState: Board.State = board.currentState
repeat {
switch board.currentState {
case .InvalidMove:
print("⚠️ Ops... Invalid move, try another a different position")
board = Game.changeState(board, newState: previousState)
continue
case .Xplay:
print("\(Board.Piece.X.rawValue) turn")
case .Oplay:
board = Game.botPlay(board)
print("Behold 🤖 playing")
print(board)
continue
default:
break
}
print("Enter row")
guard let rowInput = readLine(stripNewline: true) else { continue }
guard let rowNumber = Int(rowInput) else { continue }
guard let row = Board.Row(rawValue: rowNumber) else { continue }
print("Enter column")
guard let columnInput = readLine(stripNewline: true) else { continue }
guard let columnNumber = Int(columnInput) else { continue }
guard let column = Board.Column(rawValue: columnNumber) else { continue }
previousState = board.currentState
board = Game.play(board, row: row, column: column)
print(board)
} while board.currentState != .Finished
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment