From bcf87a10fd349ae6567adab665a36dd5223791f6 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Tue, 13 Jul 2021 15:54:54 -0700 Subject: [PATCH] multithreaded simulation for full game --- game.nim | 4 +- main.nim | 108 +++-------------------------------------- simulation.nim | 129 +++++++++++++++++++++++++++++++++++++++++++++++++ test.nim | 57 ++++++++++++++++++++++ 4 files changed, 196 insertions(+), 102 deletions(-) create mode 100644 simulation.nim create mode 100644 test.nim diff --git a/game.nim b/game.nim index 4b4776b..94f86aa 100644 --- a/game.nim +++ b/game.nim @@ -86,9 +86,9 @@ proc display*(b: Board, start, stop: int) = let sq = b.squares[i] let lead = $i & ": " if sq.tile.isSome: - echo lead, sq.tile.get + stdout.writeLine($lead & $sq.tile.get) else: - echo lead, sq.camels + stdout.writeLine($lead & $sq.camels) echo "" diff --git a/main.nim b/main.nim index 24ac901..38f0bf6 100644 --- a/main.nim +++ b/main.nim @@ -1,102 +1,5 @@ import math, options, sequtils, random, sets -import combinators, game, fixedseq, ui - - -type - ScoreSet* = array[Color, int] - - ScoreSpread = object - lo: array[Color, float] - hi: array[Color, float] - - LegResults* = tuple[scores: ScoreSet, endStates: HashSet[Board]] - - -proc update*(scores: var ScoreSet, toAdd: ScoreSet) = - for i, s in toAdd: - scores[i] += s - - -proc display*(scores: ScoreSet) = - let total = scores.sum - for color, score in scores: - echo color, ": ", round(100 * scores[color] / total, 2), '%' - - -proc projectLeg*(b: Board): LegResults = - var scores: ScoreSet - var endStates: HashSet[Board] - - var diceRemaining: ColorStack - diceRemaining.initFixedSeq - for i, c in b.diceRolled: - if not c: diceRemaining.add(i) - - for future in possibleFutures(diceRemaining): - var prediction = b # make a copy - for dieRoll in future: - prediction.advance(dieRoll) - inc scores[prediction.leader.get] - # deduplicate results - endStates.incl(prediction) - - result = (scores, endStates) - - -proc projectOutcomes(b: Board, maxDepth = 1): ScoreSet = - var outcomeStack = @[ [b].toHashSet ] - for depth in 1..maxDepth: - echo "simulating ", outcomeStack[^1].len, " possible legs." - var endStates: HashSet[Board] - - for o in outcomeStack[^1]: - var o = o # make it mutable - if outcomeStack.len > 1: - o.resetDice # o was describina an end-of-leg state, so dice were exhausted - - let projection = o.projectLeg - result.update(projection[0]) - endStates.incl(projection[1]) - stdout.write("simulated: " & $result.sum & "\r") - - outcomeStack.add(endStates) - echo "\nDistinct end states: ", outcomeStack.mapIt(it.len).sum - - -proc randomGame(b: Board, r: var Rand): Color = - var projection = b - while true: - for roll in randomFuture(projection.diceRemaining, r): - projection.advance(roll) - if projection.gameOver: - return projection.leader.get - projection.resetDice - - -proc randomGames(b: Board, count: SomeInteger): ScoreSet = - randomize() - var r = initRand(rand(int64)) - for i in 1 .. count: - let winner = b.randomGame(r) - inc result[winner] - # if i mod 100_000 == 0 or i == count - 1: - # stdout.write("simulating " & count & "random games: " & $i & "\r") - # echo "" - - -proc randomSpread(b: Board, nTests: SomeInteger, nSamples: SomeInteger): ScoreSpread = - for s in result.lo.mitems: - s = 1 - - for i in 0 ..< nTests: - let scores = b.randomGames(nSamples) - let total = scores.sum - for color, score in scores: - let pct = score / total - if pct < result.lo[color]: - result.lo[color] = pct - if pct > result.hi[color]: - result.hi[color] = pct +import combinators, game, fixedseq, simulation, ui when isMainModule: @@ -106,5 +9,10 @@ when isMainModule: b.setState(config.state, []) b.diceRolled = config.diceRolled b.display(1, 5) - let scores = b.projectLeg()[0] - scores.display + let legScores = b.getLegScores + echo "Current leg probabilities:" + legScores.display + + let gameScores = b.randomGames(1_000_000) + echo "\nFull game probabilities (1M simulations):" + gameScores.display diff --git a/simulation.nim b/simulation.nim new file mode 100644 index 0000000..b59a1f8 --- /dev/null +++ b/simulation.nim @@ -0,0 +1,129 @@ +import cpuinfo, math, options, random, tables +import combinators, game, fixedseq + + +type + ScoreSet* = array[Color, int] + + ScoreSpread = object + lo: array[Color, float] + hi: array[Color, float] + + LegResults* = tuple[scores: ScoreSet, endStates: CountTable[Board]] + + +proc update*(scores: var ScoreSet, toAdd: ScoreSet) = + for i, s in toAdd: + scores[i] += s + + +proc display*(scores: ScoreSet) = + let total = scores.sum + for color, score in scores: + let line = $color & ": " & $round(100 * scores[color] / total, 2) & '%' + stdout.writeLine(line) + stdout.flushFile() + # echo color, ": ", round(100 * scores[color] / total, 2), '%' + + +# ====================== +# Single-leg simulations +# ====================== + +iterator legEndStates(b: Board): Board = + var diceRemaining: ColorStack + diceRemaining.initFixedSeq + for i, c in b.diceRolled: + if not c: diceRemaining.add(i) + + for future in possibleFutures(diceRemaining): + var prediction = b # make a copy so we can mutate + for dieRoll in future: + prediction.advance(dieRoll) + yield prediction + + +proc getLegScores*(b: Board): ScoreSet = + for prediction in b.legEndStates: + inc result[prediction.leader.get] + + +# ===================== +# Full-game simulations +# ===================== + +proc randomGame*(b: Board, r: var Rand): Color = + var projection = b + while true: + for roll in randomFuture(projection.diceRemaining, r): + projection.advance(roll) + if projection.gameOver: + return projection.leader.get + projection.resetDice() + + +proc randomGamesWorker(b: Board, count: Natural, r: var Rand): ScoreSet = + for i in 1 .. count: + let winner = b.randomGame(r) + inc result[winner] + + +# ======================= +# Multithreading nonsense +# ======================= + +type WorkerArgs = object + board: Board + count: Natural + seed: int64 +# have to do these at the module level so they can be shared +var gamesChannel: Channel[ScoreSet] +gamesChannel.open() + + +proc randomGamesThread(args: WorkerArgs) = + var r = initRand(args.seed) + let scores = randomGamesWorker(args.board, args.count, r) + gamesChannel.send(scores) + + +proc randomGames*(b: Board, count: Natural, parallel = true, numThreads = 0): ScoreSet = + randomize() + + if not parallel: + var r = initRand(rand(int64)) + return randomGamesWorker(b, count, r) + + let numThreads = + if numThreads == 0: + countProcessors() + else: + numThreads + + var workers = newSeq[Thread[WorkerArgs]](numThreads) + for i, w in workers.mpairs: + var numGames = int(floor(count / numThreads)) + if i <= (count mod numThreads): + numGames += 1 + let args = WorkerArgs(board: b, count: numGames, seed: rand(int64)) + + createThread(w, randomGamesThread, args) + + for i in 1 .. numThreads: + let scores = gamesChannel.recv() + result.update(scores) + + +proc randomSpread*(b: Board; nTests, nSamples: Natural): ScoreSpread = + for s in result.lo.mitems: + s = 1 + + for i in 0 ..< nTests: + let scores = b.randomGames(nSamples) + let total = scores.sum + for color, score in scores: + let pct = score / total + if pct < result.lo[color]: + result.lo[color] = pct + if pct > result.hi[color]: + result.hi[color] = pct \ No newline at end of file diff --git a/test.nim b/test.nim new file mode 100644 index 0000000..12b4f73 --- /dev/null +++ b/test.nim @@ -0,0 +1,57 @@ +import random, times +import fixedseq, game, simulation + + +proc randomDice(r: var Rand): seq[tuple[c: Color, p: int]] = + for c in Color: + let v = r.rand(1..3) + result.add((c, v)) + result.shuffle + + +proc testGames(n: SomeInteger = 100): auto = + var r = initRand(rand(int64)) + let dice = randomDice(r) + var b: Board + b.init + b.setState(dice, []) + b.display(1, 5) + + let startTime = cpuTime() + let scores = b.randomGames(n) + result = cpuTime() - startTime + scores.display() + + +proc testLegs(n: Natural = 100): auto = + var boards: seq[Board] + var r = initRand(rand(int64)) + for i in 1 .. n: + var b: Board + b.init + let dice = randomDice(r) + b.setState(dice, []) + boards.add(b) + stdout.write("Constructed: " & $i & "\r") + echo "" + + echo "Running..." + let start = cpuTime() + for b in boards: + discard b.getLegScores + result = cpuTime() - start + + +when isMainModule: + randomize() + # let start_states = 2_000 + # let executionTime = testLegs(start_states) + # echo "Execution time: ", executionTime + # echo "Leg simulations per second: ", float(start_states * 29_160) / executionTime + + for i in 1 .. 1: + let num_games = 100_000_005 + let executionTime = testGames(num_games) + echo "Execution time: ", executionTime + echo "Full-game simulations per second: ", float(num_games) / executionTime + echo ""