Compare commits

..

2 Commits

Author SHA1 Message Date
Joseph Montanaro
bd413da9a3 more variance testing 2021-07-13 16:16:47 -07:00
Joseph Montanaro
bcf87a10fd multithreaded simulation for full game 2021-07-13 15:54:54 -07:00
4 changed files with 220 additions and 102 deletions

View File

@ -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 ""

108
main.nim
View File

@ -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

131
simulation.nim Normal file
View File

@ -0,0 +1,131 @@
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 this at the module level so it 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

79
test.nim Normal file
View File

@ -0,0 +1,79 @@
import math, random, strformat, 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
proc testSpread(nTests, nSamples: Natural) =
var b: Board
b.init
var r = initRand(rand(int64))
let dice = randomDice(r)
b.setState(dice, [])
b.display(1, 5)
let spread = randomSpread(b, nTests, nSamples)
stdout.writeLine("Variance:")
for c in Color:
let variance = 100 * (spread.hi[c] - spread.lo[c])
stdout.writeLine(fmt"{c}: {round(variance, 2):.2f}%")
let diff = 100 * (max(spread.hi) - min(spread.lo))
stdout.writeLine(fmt"Win percentage differential: {round(diff, 2):.2f}%")
stdout.flushFile()
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 ""
testSpread(100, 1_000_000)