2021-07-13 22:54:54 +00:00
|
|
|
import cpuinfo, math, options, random, tables
|
2021-07-17 18:02:15 +00:00
|
|
|
import combinators, game, faststack, fixedseq
|
2021-07-13 22:54:54 +00:00
|
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
ScoreSet* = array[Color, int]
|
2021-07-15 04:49:10 +00:00
|
|
|
WinPercents* = array[Color, float]
|
2021-07-13 22:54:54 +00:00
|
|
|
|
|
|
|
ScoreSpread = object
|
2021-07-13 23:16:47 +00:00
|
|
|
lo*: array[Color, float]
|
|
|
|
hi*: array[Color, float]
|
2021-07-13 22:54:54 +00:00
|
|
|
|
|
|
|
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), '%'
|
|
|
|
|
|
|
|
|
2021-07-15 04:49:10 +00:00
|
|
|
proc percents*(scores: ScoreSet): WinPercents =
|
|
|
|
let total = scores.sum
|
|
|
|
for c, score in scores:
|
|
|
|
result[c] = score / total
|
|
|
|
|
|
|
|
|
2021-07-13 22:54:54 +00:00
|
|
|
# ======================
|
|
|
|
# Single-leg simulations
|
|
|
|
# ======================
|
|
|
|
|
|
|
|
iterator legEndStates(b: Board): Board =
|
2021-07-17 15:43:26 +00:00
|
|
|
var diceRemaining: FixedSeq[5, Color, int8]
|
2021-07-13 22:54:54 +00:00
|
|
|
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
|
|
|
|
# =====================
|
|
|
|
|
2021-07-17 18:02:15 +00:00
|
|
|
# get rid of this later
|
|
|
|
import strutils
|
|
|
|
proc showSpaces*(b: Board; start, stop: Natural): string =
|
|
|
|
let numSpaces = stop - start + 1
|
|
|
|
let width = 4 * numSpaces - 1
|
|
|
|
var lines: array[7, string]
|
|
|
|
# start by building up an empty board
|
|
|
|
for i in 0 .. 6: # gotta initialize the strings
|
|
|
|
lines[i] = newString(width)
|
|
|
|
for c in lines[i].mitems:
|
|
|
|
c = ' '
|
|
|
|
# fill in the dividers
|
|
|
|
lines[5] = repeat("=== ", numSpaces - 1)
|
|
|
|
lines[5].add("===")
|
|
|
|
|
|
|
|
# now populate the board
|
|
|
|
for sp in 0 ..< numSpaces:
|
|
|
|
# fill in the square numbers
|
|
|
|
let squareNum = sp + start
|
|
|
|
let cellMid = 4 * sp + 1
|
|
|
|
for i, chr in $squareNum:
|
|
|
|
lines[6][cellMid + i] = chr
|
|
|
|
|
|
|
|
# fill in the camel stacks
|
|
|
|
for i, color in b.squares[squareNum].camels:
|
|
|
|
let lineNum = 4 - i # lines go to 6, but bottom 2 are reserved
|
|
|
|
let repr = '|' & color.abbrev & '|'
|
|
|
|
for j, chr in repr:
|
|
|
|
lines[lineNum][cellMid - 1 + j] = chr
|
|
|
|
|
|
|
|
result = lines.join("\n")
|
|
|
|
# get rid of this later
|
|
|
|
|
2021-07-13 22:54:54 +00:00
|
|
|
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
|
2021-07-13 23:16:47 +00:00
|
|
|
|
|
|
|
|
|
|
|
# have to do this at the module level so it can be shared
|
2021-07-13 22:54:54 +00:00
|
|
|
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))
|
2021-07-14 15:43:12 +00:00
|
|
|
if i < (count mod numThreads):
|
2021-07-13 22:54:54 +00:00
|
|
|
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
|