6 Commits

9 changed files with 228 additions and 42 deletions

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# `cup` - CamelUp probability calculator
This tool calculates probable outcomes for the board game CamelUp.
It can calculate all possible outcomes for a single game leg in about 5ms, so effectively instantaneously.
Full-game calculations take a little bit longer and are not exact (since it isn't practical to simulate all possible full game states.)
However it can easily simulate a million random games in about 80ms in the worst case, which should provide estimates accurate to within about 0.2%.
(Numbers from running on a Ryzen 3700X.)
```
Usage:
cup [-i] SPACE:STACK [...SPACE:STACK] [DICE]
SPACE refers to a numbered board space (1-16).
STACK refers to a stack of camel colors from bottom to top, e.g.
YBR (Yellow, Blue, Red, with Red on top).
DICE refers to the set of dice that have already been rolled,
e.g. GPR (Green, Purple, Red)
Options:
-i Interactive mode (currently unimplemented)
-h Show this message and exit
```

View File

@ -1,5 +1,5 @@
import algorithm, random, sugar
import fixedseq, game
import fastrand, fixedseq, game
proc nextPermutation(x: var FixedSeq): bool =
@ -93,4 +93,4 @@ proc randomFuture*(dice: FixedSeq, r: var Rand): FixedSeq[5, Die, int8] =
result.initFixedSeq
let order = dice.dup(shuffle(r))
for i, color in order:
result.add((color, r.rand(1..3)))
result.add((color, r.fastRand(1..3)))

5
config.nims Normal file
View File

@ -0,0 +1,5 @@
--threads: on
--d: release
--opt: speed
--passC: -flto
--passL: -flto

View File

@ -1,5 +1,4 @@
import math, options, sequtils, random, sets
import combinators, game, fixedseq, simulation, ui
import game, simulation, ui
when isMainModule:
@ -8,11 +7,11 @@ when isMainModule:
b.init
b.setState(config.state, [])
b.diceRolled = config.diceRolled
b.display(1, 5)
echo b.showSpaces(1, 16)
let legScores = b.getLegScores
echo "Current leg probabilities:"
legScores.display
echo "\nCurrent leg probabilities:"
echo legScores.showPercents()
let gameScores = b.randomGames(1_000_000)
echo "\nFull game probabilities (1M simulations):"
gameScores.display
echo gameScores.showPercents()

74
fastrand.nim Normal file
View File

@ -0,0 +1,74 @@
import random, math
import times, std/monotimes, strformat, strutils
proc formatNum(n: SomeNumber): string =
let s = $(n.round)
let t = s[0 .. s.len - 3]
var count = 1
for i in countdown(t.high, 0):
result.insert($t[i], 0)
if count mod 3 == 0 and i != 0:
result.insert(",", 0)
count += 1
proc formatRate(n: Natural, d: Duration): string =
result = formatNum(1_000_000'f64 * n.float64 / d.inMicroseconds.float64)
const upperBound = uint64(uint32.high)
proc fastRand*[T: Natural](r: var Rand, x: T): T =
# Nim ranges are usually inclusive, but this algorithm is exclusive
let x = x.uint64 + 1
let num = if x <= upperBound:
((r.next shr 32) * x.uint64) shr 32
else:
r.next mod x.uint64
result = T(num)
proc fastRand*(r: var Rand; x, y: Natural): Natural =
let lim = (y - x)
result = fastRand(r, lim) + x
proc fastRand*[T](r: var Rand, slice: HSlice[T, T]): T =
let n = fastRand(r, slice.a.Natural, slice.b.Natural)
result = T(n)
proc testFastRand(num = 1_000_000_000): Duration =
var r = initRand(rand(int64))
let start = getMonoTime()
for i in 1 .. num:
discard r.fastRand(5)
result = getMonoTime() - start
# echo "fastrand execution rate: ", 1000 * num / dur.inMilliseconds.int, " generated per second."
proc testStdRand(num = 1_000_000_000): Duration =
var r = initRand(rand(int64))
let start = getMonoTime()
for i in 1 .. num:
discard r.rand(4)
result = getMonoTime() - start
# echo "std rand execution rate: ", 1000 * num / dur.inMilliseconds.int, " generated per second."
when isMainModule:
randomize()
var r = initRand(rand(int64))
let runs = 100_000_000
var totals: array[5..9, int]
for i in 1 .. runs:
let n = r.fastRand(5..9)
totals[n] += 1
echo totals
# let fr = testFastRand(runs)
# echo "fastrand execution rate: ", formatNum(1_000_000 * runs / fr.inMicroseconds.int)
# let sr = testStdRand(runs)
# echo "standard execution rate: ", formatNum(1_000_000 * runs / sr.inMicroseconds.int)

View File

@ -18,15 +18,21 @@ proc getAllColors: ColorStack =
for c in Color.low .. Color.high:
result[i] = c
const allColors* = getAllColors()
const colorNames: array[Color, string] =
const
allColors* = getAllColors()
colorNames: array[Color, string] =
["Red", "Green", "Blue", "Yellow", "Purple"]
colorAbbrevs: array[Color, char] = ['R', 'G', 'B', 'Y', 'P']
proc `$`*(c: Color): string =
result = colorNames[c]
proc abbrev*(c: Color): char =
result = colorAbbrevs[c]
proc `$`*(s: ColorStack): string =
result.add("St@[")
for i, color in s:
@ -44,8 +50,8 @@ type
tForward = 1
Square* = object
camels: ColorStack
tile: Option[Tile]
camels*: ColorStack
tile*: Option[Tile]
Board* = object
squares*: array[1..16, Square]

View File

@ -4,6 +4,7 @@ import combinators, game, fixedseq
type
ScoreSet* = array[Color, int]
WinPercents* = array[Color, float]
ScoreSpread = object
lo*: array[Color, float]
@ -26,6 +27,12 @@ proc display*(scores: ScoreSet) =
# echo color, ": ", round(100 * scores[color] / total, 2), '%'
proc percents*(scores: ScoreSet): WinPercents =
let total = scores.sum
for c, score in scores:
result[c] = score / total
# ======================
# Single-leg simulations
# ======================

View File

@ -1,5 +1,30 @@
import math, random, strformat, times
import fixedseq, game, simulation
import math, random, strformat, times, std/monotimes
import fixedseq, game, simulation, ui
type
TestResults = object
ops: int
time: Duration
proc summarize(tr: TestResults) =
let secs = tr.time.inMilliseconds.float / 1000
stdout.write("Test completed:\n")
stdout.write(" " & $tr.ops, " operations in " & $round(secs, 2) & " seconds\n")
stdout.write(" " & $round(tr.ops.float / secs, 2) & " operations per second")
stdout.flushFile()
template executionTime(body: untyped): Duration =
let start = getMonoTime()
body
getMonoTime() - start
proc getRand(): Rand =
randomize()
result = initRand(rand(int64))
proc randomDice(r: var Rand): seq[tuple[c: Color, p: int]] =
@ -9,18 +34,24 @@ proc randomDice(r: var Rand): seq[tuple[c: Color, p: int]] =
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)
proc newRandomGame(r: var Rand): Board =
var dice: array[5, tuple[c: Color, p: int]]
for i in 0 .. 4:
dice[i] = (Color(i), r.rand(1..3))
let startTime = cpuTime()
let scores = b.randomGames(n)
result = cpuTime() - startTime
scores.display()
result.init
result.setState(dice, [])
proc games(nTests, nSamples: SomeInteger, parallel = true): TestResults =
var r = getRand()
var scores: ScoreSet
for i in 1 .. nTests:
let b = newRandomGame(r)
let dur = executionTime:
let s = b.randomGames(nSamples, parallel = parallel)
result.ops += s.sum()
result.time += dur
proc testLegs(n: Natural = 100): auto =
@ -63,17 +94,4 @@ proc testSpread(nTests, nSamples: Natural) =
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)
games(10, 10_000_000).summarize()

59
ui.nim
View File

@ -1,5 +1,5 @@
import os, strutils
import game
import os, math, strutils, strformat
import fixedseq, game, simulation
const help =
@ -19,6 +19,11 @@ Options:
-h Show this message and exit
"""
# =============================
# User input parsing/validation
# =============================
type
CmdConfig* = object
state*: seq[tuple[c: Color, p: int]]
@ -63,3 +68,53 @@ proc parseArgs*(): CmdConfig =
for c in p:
let color = parseColor(c)
result.diceRolled[color] = true
# ==========================
# Game state representations
# ==========================
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")
proc showPercents*(scores: ScoreSet): string =
var lines: array[5, string]
for color, pct in scores.percents:
let label = align($color, 7) # e.g. " Green"
var bar = repeat(" ", 20)
let percentage = round(pct * 100, 2)
# populate the progress bar
let barFill = int(round(pct * 100 / 20))
for i in 0 ..< barFill:
bar[i] = '='
lines[int(color)] = fmt"{label}: [{bar}] {percentage}%"
result = lines.join("\n")