Carlo is a Monte Carlo Tree Search (MCTS) library for turn-based games. Whereas GKMonteCarloStrategist is clunky and confusing, Carlo is simple and easy to use!
Import Carlo by adding the following line to any .swift file:
import CarloImplement Carlo by designing player, move, and game structs that conform to the CarloGamePlayer, CarloGameMove, and CarloGame protocols.
While the first two protocols don't explicitly require anything, "conforming" to them might look like this:
enum ConnectThreePlayer: Int, CarloGamePlayer, CustomStringConvertible {
case one = 1
case two = 2
var opposite: Self {
switch self {
case .one: return .two
case .two: return .one
}
}
var description: String {
"\(rawValue)"
}
}
typealias ConnectThreeMove = Int
extension ConnectThreeMove: CarloGameMove {}Conforming to CarloGame requires the following:
public protocol CarloGame: Equatable {
associatedtype Player: CarloGamePlayer
associatedtype Move: CarloGameMove
var currentPlayer: Player { get }
func availableMoves() -> [Move]
func update(_ move: Move) -> Self
func evaluate(for player: Player) -> Evaluation
}Properly implemented it might look like this:
struct ConnectThreeGame: CarloGame, CustomStringConvertible, Equatable {
typealias Player = ConnectThreePlayer
typealias Move = ConnectThreeMove
var array: Array<Int>
// REQUIRED
var currentPlayer: Player
init(length: Int = 10, currentPlayer: Player = .one) {
self.array = Array.init(repeating: 0, count: length)
self.currentPlayer = currentPlayer
}
// REQUIRED
func availableMoves() -> [Move] {
array
.enumerated()
.compactMap { $0.element == 0 ? Move($0.offset) : nil}
}
// REQUIRED
func update(_ move: Move) -> Self {
var copy = self
copy.array[move] = currentPlayer.rawValue
copy.currentPlayer = currentPlayer.opposite
return copy
}
// REQUIRED
func evaluate(for player: Player) -> Evaluation {
let player3 = three(for: player)
let oppo3 = three(for: player.opposite)
let remaining0 = array.contains(0)
switch (player3, oppo3, remaining0) {
case (true, true, _): return .draw
case (true, false, _): return .win
case (false, true, _): return .loss
case (false, false, false): return .draw
default: return .ongoing(0.5)
}
}
private func three(for player: Player) -> Bool {
var count = 0
for slot in array {
if slot == player.rawValue {
count += 1
} else {
count = 0
}
if count == 3 {
return true
}
}
return false
}
var description: String {
return array.reduce(into: "") { result, i in
result += String(i)
}
}
}Use Carlo by scaffolding a CarloTactician on a CarloGame:
typealias Computer = CarloTactician<ConnectThreeGame>Instantiate with arguments for CarloGamePlayer and a limit for the number turns to rollout in a single search iteration:
let computer = Computer(for: .two, maxRolloutDepth: 5)Call the .iterate() method to perform one search iteration in the tree, .bestMove to get the best move (so far) as found by search algorithm, and .uproot(to:) to recycle the tree and update the internal game state:
var game = ConnectThreeGame(length: 10, currentPlayer: .one)
/// 0000000000
game = game.update(4)
/// 0000100000
game = game.update(0)
/// 2000100000
game = game.update(7)
/// 2000100000
game = game.update(2)
/// 2020100000
game = game.update(9)
/// 2020100001 ... player 2 can win if move => 1
computer.uproot(to: game)
for _ in 0..<50 {
computer.iterate()
}
let move = computer.bestMove!
game = game.update(move)
/// 2220100001 ... game overIf you use Swift Package Manager adding Carlo as a dependency is as easy as adding it to the dependencies of your Package.swift:
dependencies: [
.package(url: "https://github.com/maxhumber/Carlo.git", from: "1.0.2")
]