Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Minimax algorithm with example of use in tic tac toe game #977

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions Minimax/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Minimax algorithm

<p align="center"> <img src="Resources/image1.jpg" {:height="50%" width="50%"} /> </p>

## Runtime environment
<img src="https://img.shields.io/badge/Swift-5.3-orange.svg?style=flat" /> <img src="https://img.shields.io/badge/Xcode-12.4-blue.svg?style=flat" /> <img src="https://img.shields.io/badge/MacOS-11.2.3-blue.svg?style=flat" />

## Table of contents
* [General info](#general-info)
* [Functionality](#functionality)
* [Pseudocode](#pseudocode)
* [Demo](#demo)
* [Sources](#sources)

## General info
It is an example of implementation and use ``minimax algorithm`` in ``Tic Tac Toe`` game. Minimax is an algorithm that searches deeply into all possible states in the game. There are two types of players in the algorithm. One that wants to maximize the state of the game and one that wants to minimaze the state of the game. There are three states in the tic-tac-toe game:
- `` -1 `` - if the minimizing player wins
- `` 0 `` - in case of a tie
- `` 1 `` - if the maximizing player wins

``Alpha-beta prunning`` this is a method that interrupts the search of those branches that do not lead to win. Alpha for maximizing player and beta for minimizing player. Alpha-beta prunnings reduce the time complexity of the algorithm.

Parameters:
- ``searching depth`` - how many moves in depth is to be calculated by the algorithm

Input:
- ``actual board state`` - in the form of an array for players symbols (cross/circle)
- ``two player symbols`` - cross / circle

Output:
- ``the best move for autonomus(AI) player`` - Position(row: Int, column: Int)

## Functionality
- example of use in Swift Playground with interactive UIView
- unit tests in XCode

## Pseudocode

```
function alphabeta(node, depth, α, β, maximizingPlayer) is
if depth = 0 or node is a terminal node then
return the heuristic value of node
if maximizingPlayer then
value := −∞
for each child of node do
value := max(value, alphabeta(child, depth − 1, α, β, FALSE))
if value ≥ β then
break (* β cutoff *)
α := max(α, value)
return value
else
value := +∞
for each child of node do
value := min(value, alphabeta(child, depth − 1, α, β, TRUE))
if value ≤ α then
break (* α cutoff *)
β := min(β, value)
return value
```

## Demo

<p align="center"> <img src="Resources/demo.gif" {:height="100%" width="100%"} /> </p>

## Sources
* Minimax algorithm: https://en.wikipedia.org/wiki/Minimax
* Alpha-beta prunning: https://en.wikipedia.org/wiki/Alpha–beta_pruning

## Author
Written by Michał Nowak(mnowak061)
Binary file added Minimax/Resources/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Minimax/Resources/image1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions Minimax/Sources/Minimax.playground/Contents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import UIKit
import PlaygroundSupport

let boardSize = CGSize(width: 500, height: 550)
let boardView = BoardView(frame: CGRect(origin: .zero, size: boardSize))

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = boardView
154 changes: 154 additions & 0 deletions Minimax/Sources/Minimax.playground/Sources/Model/Board/Board.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
public struct Board {
// MARK: -- Public variable's
public var size: Int

// MARK: -- Private variable's
private var table: [ [PlayerSymbol?] ]

// MARK: -- Public function's
public init(size: Int) {
self.size = size
self.table = []
self.clear()
}

public mutating func clear() {
self.table = Array(repeating: Array(repeating: PlayerSymbol.empty, count: size), count: size)
}

public func hasEmptyField() -> Bool {
for i in 0 ..< self.size {
for j in 0 ..< self.size {
if self.table[i][j] == PlayerSymbol.empty {
return true
}
}
}
return false
}

public func symbol(forPosition position: Position) -> PlayerSymbol? {
guard position.row < self.size, position.column < size else { return nil }
return self.table[position.row][position.column]
}

public mutating func makeMove(player: Player, position: Position) {
guard self.symbol(forPosition: position) == PlayerSymbol.empty else { return }
guard self.symbol(forPosition: position) != player.symbol else { return }

self.table[position.row][position.column] = player.symbol
}

public func check(player: Player) -> BoardStatus {
let playerSymbol: PlayerSymbol = player.symbol

if self.foundWinInRows(playerSymbol) { return BoardStatus.win }
if self.foundWinInColumns(playerSymbol) { return BoardStatus.win }
if self.foundWinInSlants(playerSymbol) { return BoardStatus.win }

if self.hasEmptyField() { return BoardStatus.continues } else { return BoardStatus.draw }
}

// MARK: -- Private function's
private func foundWinInRows(_ playerSymbol: PlayerSymbol) -> Bool {
for i in 0 ..< self.size {
var theSameSymbolsInRowCount = 0

for j in 0 ..< self.size - 1 {
if self.table[i][j] == self.table[i][j+1] && (self.table[i][j] == playerSymbol) {
theSameSymbolsInRowCount += 1
} else {
theSameSymbolsInRowCount = 0
}
}

if theSameSymbolsInRowCount == self.size - 1 {
return true
}
}

return false
}

private func foundWinInColumns(_ playerSymbol: PlayerSymbol) -> Bool {
for j in 0 ..< self.size {
var theSameSymbolsInColumnCount = 0

for i in 0 ..< self.size - 1 {
if self.table[i][j] == self.table[i+1][j] && (self.table[i][j] == playerSymbol) {
theSameSymbolsInColumnCount += 1
} else {
theSameSymbolsInColumnCount = 0
}
}

if theSameSymbolsInColumnCount == self.size - 1 {
return true
}
}

return false
}

private func foundWinInSlants(_ playerSymbol: PlayerSymbol) -> Bool {
var theSameSymbolsInSlantCount = 0

for i in 0 ..< self.size {
for j in -(self.size - 1) ... 0 {
if(self.table[-j][i] == playerSymbol) {
var k: Int = -j
var l: Int = i
theSameSymbolsInSlantCount = 0

while l < self.size && k >= 0 {
if self.table[k][l] == playerSymbol {
theSameSymbolsInSlantCount += 1
} else {
theSameSymbolsInSlantCount = 0
}
k -= 1
l += 1

if theSameSymbolsInSlantCount == self.size {
return true
}
}
theSameSymbolsInSlantCount = 0
}
theSameSymbolsInSlantCount = 0
}
theSameSymbolsInSlantCount = 0
}

theSameSymbolsInSlantCount = 0

for i in 0 ..< self.size {
for j in 0 ..< self.size {
if(self.table[j][i] == playerSymbol) {
var k: Int = j
var l: Int = i
theSameSymbolsInSlantCount = 0

while l < self.size && k < self.size {
if self.table[k][l] == playerSymbol {
theSameSymbolsInSlantCount += 1
} else {
theSameSymbolsInSlantCount = 0
}
k += 1
l += 1

if theSameSymbolsInSlantCount == self.size {
return true
}
}
theSameSymbolsInSlantCount = 0
}
theSameSymbolsInSlantCount = 0
}
theSameSymbolsInSlantCount = 0
}

return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public typealias Position = (row: Int, column: Int)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public enum BoardStatus {

case continues

case win

case draw
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public enum DifficultLevel: Int {

case easy = 2

case medium = 3

case hard = 5
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Foundation

public class GameModel {
// MARK: -- Public variable's
public var board: Board!

public var gameStatus: BoardStatus

// MARK: -- Private variable's
private var playersList: [Player]!

private var movementsSequence: [Int]!

private var actualPlayerIndex: Int!

private var actualPlayer: Player {
get {
return playersList[actualPlayerIndex]
}
}

private var difficultLevel: DifficultLevel = DifficultLevel.hard

// MARK: -- Public function's
public init(boardSize: Int, playersList: [Player], difficultLevel: DifficultLevel) {
self.board = Board.init(size: boardSize)
self.playersList = playersList
self.difficultLevel = difficultLevel
self.gameStatus = BoardStatus.continues

self.generateMovementsSequence()
self.changeActualPlayer()
}

public func update() {
self.gameStatus = board.check(player: actualPlayer)

switch self.gameStatus {
case BoardStatus.continues:
changeActualPlayer()
case BoardStatus.draw:
changeActualPlayer()
default: break
}
}

public func playerMakeMove(selectedPosition: (row: Int, column: Int)) {
guard board.symbol(forPosition: selectedPosition) == PlayerSymbol.empty else { return }
guard board.hasEmptyField() == true else { return }

board.makeMove(player: actualPlayer, position: selectedPosition)
update()
}

public func makeMinimaxMove() {
guard actualPlayer.type == PlayerType.computer else { return }
guard board.hasEmptyField() == true else { return }

sleep(1)

let selectedPosition: Position = minimaxMove(board: board, player: playersList[0], opponent: playersList[1], depth: self.difficultLevel.rawValue)
board.makeMove(player: actualPlayer, position: selectedPosition)
update()
}

public func newRound() {
board.clear()
gameStatus = BoardStatus.continues
generateMovementsSequence()
changeActualPlayer()
}

// MARK: -- Private function's
private func generateMovementsSequence() {
self.movementsSequence = []

let playersCount = playersList.count
let movesCount = (board.size * board.size)

var move = Int.random(in: 0 ..< playersCount)
movementsSequence.append(move)

for _ in 0 ..< movesCount - 1 {
move += 1
movementsSequence.append(move % playersCount)
}
}

private func changeActualPlayer() {
if !movementsSequence.isEmpty {
actualPlayerIndex = movementsSequence.first!
movementsSequence.removeFirst()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
public enum GameStateValue: Int {

case min = -1

case null = 0

case max = 1
}
Loading