Compare commits

...

13 Commits

Author SHA1 Message Date
0c7bbbe200 switch to fastrand 2023-01-02 21:21:02 -08:00
73f70e4fa9 RIIR 2023-01-02 10:34:30 -08:00
8715e5e354 track length instead of last index for fixedseq 2022-01-11 15:26:35 -08:00
Joseph Montanaro
63df46eee6 reuse dice for subsequent legs in randomGame 2021-11-05 10:27:09 -07:00
d9efaf33e7 clear tiles as well as camels when setting state 2021-11-02 18:09:18 -07:00
Joseph Montanaro
4995a9388b fix game state representation 2021-11-01 15:44:21 -07:00
Joseph Montanaro
0ea49d3534 pull help message from readme 2021-10-22 12:35:09 -07:00
Joseph Montanaro
ad29f04660 fix percentage bar fill 2021-10-22 10:35:24 -07:00
29e3e70712 skip bounds checking in danger mode 2021-07-29 19:34:56 -07:00
Joseph Montanaro
7301597789 use cligen for benchmarking 2021-07-20 16:16:01 -07:00
e5e90a6ca5 more bench improvements 2021-07-19 21:26:26 -07:00
Joseph Montanaro
38e9e23dea no need to set winner until game is over 2021-07-19 13:21:14 -07:00
Joseph Montanaro
94c4240d63 some improvements to benchmark script 2021-07-19 11:08:13 -07:00
15 changed files with 796 additions and 785 deletions

4
.gitignore vendored
View File

@ -1,2 +1,2 @@
*.exe /target
profile_results.txt *.etl*

90
Cargo.lock generated Normal file
View File

@ -0,0 +1,90 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cup"
version = "0.1.0"
dependencies = [
"enum-map",
"fastrand",
]
[[package]]
name = "enum-map"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50c25992259941eb7e57b936157961b217a4fc8597829ddef0596d6c3cd86e1a"
dependencies = [
"enum-map-derive",
]
[[package]]
name = "enum-map-derive"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4da76b3b6116d758c7ba93f7ec6a35d2e2cf24feda76c6e38a375f4d5c59f2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "fastrand"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
dependencies = [
"instant",
]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]]
name = "proc-macro2"
version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"

17
Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "cup"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
enum-map = "2.4.2"
fastrand = "1.8.0"
# [profile.release]
# lto = true
[profile.perf]
inherits = "release"
debug = true

View File

@ -1,10 +1,12 @@
# `cup` - CamelUp probability calculator # `cup` - CamelUp probability calculator
This tool calculates probable outcomes for the board game CamelUp. This tool calculates probable outcomes for the board game CamelUp. It can
It can calculate all possible outcomes for a single game leg in about 5ms, so effectively instantaneously. calculate all possible outcomes for a single game leg in about 5ms, so
Full-game calculations take a little bit longer and are not exact (since it isn't practical to simulate all possible full game states.) effectively instantaneously. Full-game calculations take a little bit longer
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%. and are not exact (since it isn't practical to simulate all possible full
(Numbers from running on a Ryzen 3700X.) 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: Usage:

View File

@ -1,96 +0,0 @@
import algorithm, random, sugar
import fixedseq, game
proc nextPermutation(x: var FixedSeq): bool =
# copied shamelessly from std/algorithm.nim
if x.len < 2:
return false
var i = x.high
while i > 0 and x[i - 1] >= x[i]:
dec i
if i == 0:
return false
var j = x.high
while j >= i and x[j] <= x[i - 1]:
dec j
swap x[j], x[i - 1]
x.reverse(i, x.high)
result = true
proc prevPermutation(x: var FixedSeq): bool =
# copied shamelessly from std/algorithm.nim
if x.len < 2:
return false
var i = x.high
while i > 0 and x[i - 1] <= x[i]:
dec i
if i == 0:
return false
x.reverse(i, x.high)
var j = x.high
while j >= i and x[j - 1] < x[i - 1]:
dec j
swap x[i - 1], x[j]
result = true
iterator allPermutations*(x: FixedSeq): FixedSeq =
# returns all permutations of a given seq. Order is wonky but we don't care.
var workingCopy = x
yield workingCopy
while workingCopy.nextPermutation: # this mutates workingCopy
yield workingCopy
workingCopy = x
while workingCopy.prevPermutation:
yield workingCopy
iterator allDigits*(lo, hi, size: Natural): auto =
if size > 0: # otherwise we get an infinite loop
var digits: FixedSeq[5, int, int8]
digits.initFixedSeq
for i in 0 ..< size:
digits.add(lo)
var complete = false
while not complete:
yield digits
for i in countdown(digits.high, 0):
if digits[i] < hi:
inc digits[i]
break
elif i == 0: # since this is the last digit to be incremented, we must be done
complete = true
else:
digits[i] = lo
iterator possibleFutures*(dice: FixedSeq): auto =
# iterate over all possible sequences of die rolls. Each outcome
# is returned as a 5-sequence of (color, number) tuples.
for perm in dice.allPermutations:
for digits in allDigits(1, 3, dice.len):
var f = initFixedSeq(5, Die, int8)
for i in 0 .. dice.high:
f.add((perm[i], digits[i]))
yield f
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)))

View File

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

17
cup.nim
View File

@ -1,17 +0,0 @@
import game, simulation, ui
when isMainModule:
let config = parseArgs()
var b: Board
b.init
b.setState(config.state, [])
b.diceRolled = config.diceRolled
echo b.showSpaces(1, 16)
let legScores = b.getLegScores
echo "\nCurrent leg probabilities:"
echo legScores.showPercents()
let gameScores = b.randomGames(1_000_000)
echo "\nFull game probabilities (1M simulations):"
echo gameScores.showPercents()

View File

@ -1,146 +0,0 @@
import random
type
FixedSeq*[Idx: static int; Contents; Pointer: SomeSignedInt] = object
data: array[Idx, Contents]
last: Pointer
proc initFixedSeq*(size: static Natural; cType: typedesc; pType: typedesc[SomeSignedInt]): auto =
var s: FixedSeq[size, cType, pType]
s.last = -1
result = s
proc initFixedSeq*(s: var FixedSeq) =
s.last = -1
proc `$`*(s: FixedSeq): string =
result.add("FixedSeq[")
for i, item in s:
if i != 0:
result.add(", ")
result.add($item)
result.add("]")
proc `[]`*(s: FixedSeq, i: Natural): FixedSeq.Contents =
if i > s.last:
raise newException(IndexDefect, "index " & $i & " is out of bounds.")
s.data[i]
proc `[]`*(s: var FixedSeq, i: Natural): var FixedSeq.Contents =
if i > s.last:
raise newException(IndexDefect, "index " & $i & " is out of bounds.")
s.data[i]
proc `[]`*(s: FixedSeq, i: BackwardsIndex): auto =
if s.last == -1:
raise newException(IndexDefect, "index out of bounds, the container is empty.") # matching stdlib again
s.data[s.last - typeof(s.last)(i) + 1]
proc `[]=`*(s: var FixedSeq, i: Natural, v: FixedSeq.Contents) =
if i > s.last:
raise newException(IndexDefect, "index " & $i & " is out of bounds.")
s.data[i] = v
proc high*(s: FixedSeq): auto =
result = s.last
proc low*(s: FixedSeq): auto =
result = case s.last
of -1: 0 # a bit weird but it's how the stdlib seq works
else: s.last
proc len*(s: FixedSeq): auto =
result = s.last + 1
iterator items*(s: FixedSeq): auto =
for i in 0 .. s.last:
yield s.data[i]
iterator asInt*(s: FixedSeq): int8 =
for i in 0 .. s.last:
yield int8(s.data[i]) # now we do have to convert
iterator pairs*(s: FixedSeq): auto =
var count = 0
for c in s:
yield (count, c)
inc count
proc add*(s: var FixedSeq, v: FixedSeq.Contents) =
let i = s.last + 1
s.data[i] = v # will raise exception if out of bounds
s.last = i
proc insert*(s: var FixedSeq, v: FixedSeq.Contents, idx: Natural = 0) =
for i in countdown(s.last, typeof(s.last)(idx)):
swap(s.data[i], s.data[i + 1]) # will also raise exception if out of bounds
s.data[idx] = v
inc s.last
proc delete*(s: var FixedSeq, idx: Natural) =
if idx > s.last:
raise newException(IndexDefect, "index " & $idx & " is out of bounds.")
s.data[idx] = -1
dec s.last
for i in typeof(s.last)(idx) .. s.last:
swap(s.data[i], s.data[i + 1])
proc find*(s: FixedSeq, needle: FixedSeq.Contents): FixedSeq.Pointer =
for i, v in s.data:
if v == needle:
return i
return -1
proc reverse*(s: var FixedSeq; first, last: Natural) =
# copied shamelessly from std/algorithm.nim
var x = first
var y = last
while x < y:
swap(s[x], s[y])
inc x
dec y
proc shuffle*(s: var FixedSeq, r: var Rand) =
r.shuffle(s.data)
proc moveSubstack*(src, dst: var FixedSeq; start: Natural) =
var count: typeof(src.last) = 0 # have to track this separately apparently
for idx in start .. src.last:
swap(src.data[idx], dst.data[dst.last + 1 + count])
inc count
dst.last += count
src.last -= count
proc moveSubstackPre*(src, dst: var FixedSeq; start: Natural) =
let ssLen = typeof(src.last)(src.last - start + 1) # length of substack
for i in countdown(dst.last, 0):
swap(dst.data[i], dst.data[i + ssLen])
var count = 0
for i in start .. src.last:
swap(src.data[i], dst.data[count])
inc count
dst.last += ssLen
src.last -= ssLen

161
game.nim
View File

@ -1,161 +0,0 @@
import hashes, options
import fixedseq
type
Color* = enum
cRed, cGreen, cBlue, cYellow, cPurple
ColorStack* = FixedSeq[5, Color, int8]
proc initColorStack*: ColorStack =
result.initFixedSeq
proc getAllColors: ColorStack =
var i = 0
for c in Color.low .. Color.high:
result[i] = c
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:
result.add($color)
if i < s.high:
result.add(", ")
result.add("]")
type
Die* = tuple[color: Color, value: int]
Tile* = enum
tBackward = -1,
tForward = 1
Square* = object
camels*: ColorStack
tile*: Option[Tile]
Board* = object
squares*: array[1..16, Square]
camels*: array[Color, range[1..16]]
diceRolled*: array[Color, bool]
leader*: Option[Color]
gameOver*: bool
initialized: bool
# use a template here for better inlining
template `[]`*[T](b: var Board, idx: T): var Square =
b.squares[idx]
proc hash*(b: Board): Hash =
var h: Hash = 0
# there could be a tile anywhere so we have to check all squares
for i, sq in b.squares:
if sq.camels.len > 0 or sq.tile.isSome:
h = h !& i
if sq.tile.isSome:
h = h !& int(sq.tile.get) * 10 # so it isn't confused with a camel
else:
for c in sq.camels.asInt:
h = h !& c
result = !$h
proc init*(b: var Board) =
for sq in b.squares.mitems:
sq.camels.initFixedSeq
b.initialized = true
proc display*(b: Board, start, stop: int) =
for i in start..stop:
let sq = b.squares[i]
let lead = $i & ": "
if sq.tile.isSome:
stdout.writeLine($lead & $sq.tile.get)
else:
stdout.writeLine($lead & $sq.camels)
echo ""
proc setState*(b: var Board;
camels: openArray[tuple[c: Color, p: int]];
tiles: openArray[tuple[t: Tile, p: int]]) =
for (color, dest) in camels: # note that `camels` is ordered, as this determines stacking
b[dest].camels.add(color)
b.camels[color] = dest
for (tile, dest) in tiles:
b[dest].tile = some(tile)
let leadCamel = b[max(b.camels)].camels[^1] # top camel in the last currently-occupied space
b.leader = some(leadCamel)
proc diceRemaining*(b: Board): ColorStack =
result.initFixedSeq
for color, isRolled in b.diceRolled:
if not isRolled: result.add(color)
proc resetDice*(b: var Board) =
for c, rolled in b.diceRolled:
b.diceRolled[c] = false
proc advance*(b: var Board, die: Die) =
let
(color, roll) = die
startPos = b.camels[color]
var endPos = startPos + roll
if endPos > 16: # camel has passed the finish line
b.leader = some(b[startPos].camels[^1])
b.gameOver = true
return
var prepend = false
if b[endPos].tile.isSome: # adjust position (and possibly stacking) to account for tile
let t = b[endPos].tile.get
endPos += int(t)
if t == tBackward: prepend = true
let stackStart = b[startPos].camels.find(color)
if prepend:
b[startPos].camels.moveSubstackPre(b[endPos].camels, stackStart)
let stackLen = b[startPos].camels.len - stackStart
for i in 0 ..< stackLen:
# we know how many camels we added to the bottom, so set the position for each of those
b.camels[b[endPos].camels[i]] = endPos
else:
let dstPrevHigh = b[endPos].camels.high
b[startPos].camels.moveSubstack(b[endPos].camels, stackStart)
# the camels that have moved start immediately after the previous high camel
for i in (dstPrevHigh + 1) .. b[endPos].camels.high:
b.camels[b[endPos].camels[i]] = endPos
# if we are stacking on or moving past the previous leader
if endPos >= b.camels[b.leader.get]:
b.leader = some(b[endPos].camels[^1])
b.diceRolled[color] = true

View File

@ -1,138 +0,0 @@
import cpuinfo, math, options, random, tables
import combinators, game, fixedseq
type
ScoreSet* = array[Color, int]
WinPercents* = array[Color, float]
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), '%'
proc percents*(scores: ScoreSet): WinPercents =
let total = scores.sum
for c, score in scores:
result[c] = score / total
# ======================
# 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

375
src/game.rs Normal file
View File

@ -0,0 +1,375 @@
use enum_map::{Enum, EnumMap};
use fastrand::Rng;
use crate::stack::Stack;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Enum)]
pub enum Color {
#[default] Red, Green, Blue, Yellow, Purple,
}
// const COLORS: Stack<Color, 5> = Stack::from_array([
// Color::Red,
// Color::Green,
// Color::Blue,
// Color::Yellow,
// Color::Purple,
// ]);
const COLORS: [Color; 5] = [
Color::Red,
Color::Green,
Color::Blue,
Color::Yellow,
Color::Purple,
];
type ColorStack = Stack<Color, 5>;
#[derive(Debug, Copy, Clone)]
pub enum Tile {
Forward,
Backward,
}
#[derive(Debug, Copy, Clone)]
pub enum Square {
Camels(ColorStack),
Tile(Tile),
}
impl Square {
fn assume_stack(&self) -> &ColorStack {
match self {
Square::Camels(stack) => stack,
_ => panic!("Attempted to use the stack from a non-stack square"),
}
}
fn assume_stack_mut(&mut self) -> &mut ColorStack {
match self {
Square::Camels(stack) => stack,
_ => panic!("Attempted to use the stack from a non-stack square"),
}
}
}
impl Default for Square {
fn default() -> Self {
Square::Camels(ColorStack::new())
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct Game {
squares: [Square; 16],
dice: EnumMap<Color, bool>,
camels: EnumMap<Color, usize>,
}
impl Game {
pub fn new() -> Self {
Self::default()
}
// new game with random starting positions
pub fn new_random() -> Self {
let mut game = Self::default();
let rng = Rng::new();
let mut dice = *&COLORS;
rng.shuffle(&mut dice);
for color in dice {
let roll = rng.usize(1..=3);
game.squares[roll - 1].assume_stack_mut().push(color);
game.camels[color] = roll - 1;
}
game
}
pub fn set_state(&mut self, camels: &[(Color, usize); 5], dice: &EnumMap<Color, bool>) {
for i in 0..16 {
self.squares[i] = match self.squares[i] {
Square::Camels(mut stack) => {
stack.clear();
Square::Camels(stack)
},
_ => Square::Camels(Stack::new())
};
}
for square in self.squares {
assert_eq!(square.assume_stack().len(), 0)
}
self.dice = *dice;
for &(color, sq) in camels {
self.squares[sq].assume_stack_mut().push(color);
self.camels[color] = sq;
}
}
pub fn get_state(&self) -> ([(Color, usize); 5], EnumMap<Color, bool>) {
let mut state = [(Color::Red, 0); 5];
let mut j = 0;
for (sq_idx, square) in self.squares.iter().enumerate() {
if let Square::Camels(stack) = square {
for camel in stack.iter() {
state[j] = (*camel, sq_idx);
j += 1;
}
}
}
(state, self.dice)
}
// returns winner if there is one
pub fn advance(&mut self, die: Color, roll: usize) -> Option<Color> {
let src_sq = self.camels[die];
let dst_sq = src_sq + roll;
if dst_sq >= 16 {
self.dice[die] = true;
return self.squares[src_sq].assume_stack().last().copied();
}
// special case when the destination square is the same as the source square
if let Square::Tile(Tile::Backward) = self.squares[dst_sq] {
if roll == 1 {
let src_stack = self.squares[src_sq].assume_stack_mut();
let slice_start = src_stack.iter().position(|&c| c == die).unwrap();
src_stack.shift_slice_under(slice_start);
}
}
else {
// we have to split self.squares into two slices using split_at_mut, otherwise
// rustc complains that we're trying to use two mutable references to the same value
let (left, right) = self.squares.split_at_mut(src_sq + 1);
let src_stack = left[src_sq].assume_stack_mut();
let slice_start = src_stack.iter().position(|&c| c == die).unwrap();
// since `right` starts immediately after the source square, the index of the
// destination square will be roll - 1 (e.g. if roll is 1, dst will be right[0])
let (dst_rel_idx, prepend) = match right[roll - 1] {
Square::Tile(Tile::Forward) => (roll, false), // roll - 1 + 1
Square::Tile(Tile::Backward) => (roll - 2, true), // roll is guaranteed to be >= 2 since we already handled roll == 1
_ => (roll - 1, false),
};
let dst_stack = right[dst_rel_idx].assume_stack_mut();
let dst_true_idx = src_sq + 1 + dst_rel_idx; // src_sq + 1 was the original split boundary, so add the relative index to that to get the true index
if prepend {
let slice_len = src_stack.len() - slice_start;
src_stack.move_slice_under(dst_stack, slice_start);
for i in 0..slice_len {
self.camels[dst_stack[i]] = dst_true_idx;
}
}
else {
let dst_prev_len = dst_stack.len();
src_stack.move_slice(dst_stack, slice_start);
for i in dst_prev_len..dst_stack.len() {
self.camels[dst_stack[i]] = dst_true_idx;
}
}
}
self.dice[die] = true;
None
}
fn finish_leg_random(&mut self, rng: &Rng) -> Option<Color> {
let mut leg_dice: Stack<Color, 5> = Stack::new();
for (color, rolled) in self.dice {
if !rolled {
leg_dice.push(color);
}
}
rng.shuffle(&mut leg_dice[..]);
for color in leg_dice.iter() {
let roll = rng.usize(1..=3);
if let Some(winner) = self.advance(*color, roll) {
return Some(winner);
}
}
None
}
fn finish_game_random(&mut self, rng: &Rng) -> Color {
if let Some(winner) = self.finish_leg_random(rng) {
return winner;
}
let mut dice = COLORS; // makes a copy of the constant
// we are now guaranteed to be at the start of a new leg,
// so we don't need to check the dice state
loop {
// easiest if we shuffle at the start of the leg
rng.shuffle(&mut dice);
for i in 0..5 {
let roll = rng.usize(1..=3);
if let Some(winner) = self.advance(dice[i], roll) {
return winner;
}
}
}
}
pub fn project_outcomes(&self, count: usize) -> EnumMap<Color, usize> {
let (orig_camels, orig_dice) = self.get_state();
let mut projection = *self;
let mut scores: EnumMap<Color, usize> = EnumMap::default();
let rng = Rng::new();
for i in 0..count {
let winner = projection.finish_game_random(&rng);
scores[winner] += 1;
projection.set_state(&orig_camels, &orig_dice);
}
scores
}
}
#[cfg(test)]
mod test {
use super::*;
use Color::*;
#[test]
fn test_advance() {
let mut game = Game::new();
// all dice are false (not rolled) to start with
assert_eq!(game.dice.values().any(|&v| v), false);
let camel_state = [
(Blue, 0),
(Yellow, 0),
(Red, 1),
(Green, 2),
(Purple, 2),
];
game.set_state(&camel_state, &Default::default());
assert_eq!(game.squares[0].assume_stack(), &Stack::from([Blue, Yellow]));
assert_eq!(game.camels[Blue], 0);
assert_eq!(game.camels[Yellow], 0);
assert_eq!(game.squares[1].assume_stack(), &Stack::from([Red]));
assert_eq!(game.camels[Red], 1);
assert_eq!(game.squares[2].assume_stack(), &Stack::from([Green, Purple]));
assert_eq!(game.camels[Green], 2);
assert_eq!(game.camels[Purple], 2);
// BY, R, GP
game.advance(Yellow, 2);
assert_eq!(game.dice[Yellow], true);
assert_eq!(game.camels[Yellow], 2);
assert_eq!(game.squares[2].assume_stack(), &Stack::from([Green, Purple, Yellow]));
// B, R, GPY
game.advance(Red, 2);
assert_eq!(game.dice[Red], true);
assert_eq!(game.camels[Red], 3);
// B, _, GPY, R
game.advance(Purple, 1);
assert_eq!(game.dice[Purple], true);
assert_eq!(game.squares[3].assume_stack(), &Stack::from([Red, Purple, Yellow]));
// B, _, G, RPY
}
#[test]
fn test_new_random() {
for _ in 0..100 {
let game = Game::new_random();
for (camel, i) in game.camels {
assert!(i < 3); // since we've only rolled the die once for each camel
let stack = game.squares[i].assume_stack();
assert!(stack[..].contains(&camel));
}
}
}
#[test]
fn test_finish_leg() {
let mut game = Game::new();
let camel_state = [
(Purple, 0),
(Blue, 0),
(Green, 1),
(Red, 1),
(Yellow, 2),
];
game.set_state(&camel_state, &Default::default());
// PB, G, RY
game.advance(Green, 2);
// PB, _, RY, G
game.advance(Purple, 1);
// _, PB, RY, G
// since this is randomized, we should do it a bunch of times to make sure
for _ in 0..100 {
let mut projection = game; // copy?
assert_eq!(projection.squares[1].assume_stack(), &Stack::from([Purple, Blue]));
let rng = Rng::new();
projection.finish_leg_random(&rng);
// since we already rolled Green, it can't have moved
assert_eq!(projection.camels[Green], 3);
// likewise purple
assert_eq!(projection.camels[Purple], 1);
// blue, red,and yellow, on the other hand, *must* have moved
assert_ne!(projection.camels[Blue], 1);
assert_ne!(projection.camels[Red], 2);
assert_ne!(projection.camels[Yellow], 2);
}
}
#[test]
fn test_finish_leg_winner() {
let mut game = Game::new();
let camel_state = [
(Green, 13),
(Red, 14),
(Purple, 14),
(Blue, 15),
(Yellow, 15),
];
game.set_state(&camel_state, &Default::default());
// since there are no tiles involved, and multiple camels are on 15, there must be a winner
let rng = Rng::new();
assert!(matches!(game.finish_leg_random(&rng), Some(_)));
}
#[test]
fn test_project_outcomes() {
let mut game = Game::new();
let camel_state = [
(Blue, 1),
(Green, 2),
(Yellow, 2),
(Purple, 4),
(Red, 10),
];
game.set_state(&camel_state, &Default::default());
// _, B, GY, _, P, _, _, _, _, _, R
let scores = game.project_outcomes(10_000);
let mut max = 0;
let mut winner = Blue; // just "anything that's not red"
for (color, score) in scores {
if score > max {
max = score;
winner = color;
}
}
assert_eq!(winner, Red);
}
}

40
src/main.rs Normal file
View File

@ -0,0 +1,40 @@
use std::time::Instant;
mod stack;
mod game;
use game::{Game, Color::*};
fn main() {
let n_games = 200_000;
let game = Game::new_random();
// let mut game = Game::new();
// let camel_state = [
// (Blue, 5),
// (Purple, 5),
// (Red, 7),
// (Yellow, 8),
// (Green, 10),
// ];
// game.set_state(&camel_state, &Default::default());
let start = Instant::now();
let scores = game.project_outcomes(n_games);
let end = Instant::now();
let elapsed = end.duration_since(start);
let secs = (elapsed.as_secs_f64() * 100f64).round() / 100f64;
let rate = (n_games as f64) / elapsed.as_secs_f64();
println!("Test completed:");
println!("{n_games} in {secs} seconds", );
println!("Games per second: {rate}\n");
let total = scores.values().sum::<usize>() as f64;
for (color, score) in scores {
let fract = (score as f64) / total;
let pct = (fract * 10_000f64).round() / 100f64;
println!("{color:?}: {pct}%");
}
}

265
src/stack.rs Normal file
View File

@ -0,0 +1,265 @@
use std::ops::{Index, IndexMut, RangeFull};
use std::iter::IntoIterator;
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub struct Stack<T, const S: usize> {
data: [T; S],
len: usize // we can experiment with using u8 some other time
}
impl<T, const S: usize> Stack<T, S> {
pub fn push(&mut self, v: T) {
self.data[self.len] = v;
self.len += 1;
}
pub fn len(&self) -> usize {
self.len
}
pub fn clear(&mut self) {
self.len = 0;
}
pub fn last(&self) -> Option<&T> {
if self.len == 0 {
None
}
else {
Some(&self.data[self.len - 1])
}
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
self.data.iter().take(self.len)
}
pub const fn from_array(array: [T; S]) -> Self {
Stack {
data: array,
len: S,
}
}
pub fn into_inner(self) -> [T; S] {
self.data
}
}
impl<T, const S: usize> Stack<T, S>
where T: Copy + Default
{
pub fn new() -> Self {
Stack {
data: [Default::default(); S],
len: 0,
}
}
pub fn move_slice(&mut self, dst: &mut Self, start: usize) {
let slice_len = self.len - start;
let src_slice = &mut self.data[start..self.len];
let dst_slice = &mut dst.data[dst.len..(dst.len + slice_len)];
dst_slice.copy_from_slice(src_slice);
self.len -= slice_len;
dst.len += slice_len;
}
pub fn move_slice_under(&mut self, dst: &mut Self, start: usize) {
let slice_len = self.len - start;
let src_slice = &mut self.data[start..self.len];
dst.data.rotate_right(slice_len);
let dst_slice = &mut dst.data[0..slice_len];
dst_slice.copy_from_slice(src_slice);
self.len -= slice_len;
dst.len += slice_len;
}
// like above, except source and destination are the same, i.e. reordering the stack
pub fn shift_slice_under(&mut self, start: usize) {
for mut i in start..self.len {
while i > 0 {
self.data.swap(i, i -1);
i -= 1;
}
}
}
}
impl<T, const S: usize> Default for Stack<T, S>
where T: Copy + Default
{
fn default() -> Self {
Self::new()
}
}
impl<T, const S: usize> Index<usize> for Stack<T, S> {
type Output = T;
fn index(&self, index: usize) -> &T {
&self.data[index]
}
}
impl<T, const S: usize> Index<RangeFull> for Stack<T, S> {
type Output = [T];
fn index(&self, _index: RangeFull) -> &[T] {
&self.data[..self.len]
}
}
impl<T, const S: usize> IndexMut<RangeFull> for Stack<T, S> {
fn index_mut(&mut self, _index: RangeFull) -> &mut [T] {
&mut self.data[..self.len]
}
}
impl<I, T, const S: usize> From<I> for Stack<T, S>
where
T: Copy + Default,
I: IntoIterator<Item = T>
{
fn from(src: I) -> Self {
let mut res = Self::new();
for (i, item) in src.into_iter().enumerate() {
if i >= S {
break;
}
res.push(item);
}
res
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_basic() {
let mut stack: Stack<usize, 5> = Stack::new();
stack.push(1);
stack.push(2);
stack.push(3);
assert_eq!(stack.len(), 3);
assert_eq!(stack[0], 1);
assert_eq!(stack[1], 2);
assert_eq!(stack[2], 3);
assert_eq!(stack.last(), Some(&3));
stack.clear();
assert_eq!(stack.len(), 0);
}
#[test]
fn test_move_slice() {
let mut a: Stack<usize, 5> = Stack::new();
let mut b: Stack<usize, 5> = Stack::new();
a.push(1);
a.push(2);
a.push(3);
b.push(9);
b.push(8);
a.move_slice(&mut b, 1);
assert_eq!(b[2], 2);
assert_eq!(b[3], 3);
b.move_slice(&mut a, 1);
assert_eq!(a[1], 8);
assert_eq!(a[2], 2);
assert_eq!(a[3], 3);
a.move_slice(&mut b, 0);
assert_eq!(a.len(), 0);
assert_eq!(b[0], 9);
assert_eq!(b.last(), Some(&3));
}
#[test]
fn test_move_slice_under() {
let mut a: Stack<usize, 5> = Stack::new();
let mut b: Stack<usize, 5> = Stack::new();
a.push(1);
a.push(2);
a.push(3);
b.push(9);
b.push(8);
a.move_slice_under(&mut b, 1);
assert_eq!(a.len(), 1);
assert_eq!(a[0], 1);
assert_eq!(b.len(), 4);
assert_eq!(b[0], 2);
assert_eq!(b[3], 8);
b.move_slice_under(&mut a, 0);
assert_eq!(b.len(), 0);
assert_eq!(a[0], 2);
assert_eq!(a[4], 1);
}
fn test_shift_slice_under() {
let mut a: Stack<usize, 5> = Stack::from([1, 2, 3, 4, 5]);
a.shift_slice_under(3);
assert_eq!(a[0], 4);
assert_eq!(a[1], 5);
assert_eq!(a[2], 1);
assert_eq!(a[3], 2);
assert_eq!(a[4], 3);
}
#[test]
fn test_from_iter() {
let s = Stack::<_, 5>::from([1, 2, 3]);
assert_eq!(s[0], 1);
assert_eq!(s[2], 3);
let s = Stack::<_, 2>::from([1, 2, 3]);
assert_eq!(s.len(), 2);
assert_eq!(s[0], 1);
assert_eq!(s[1], 2);
}
#[test]
fn test_iter() {
let s = Stack::<_, 5>::from([1, 2, 3]);
let mut it = s.iter();
assert_eq!(it.next(), Some(&1));
assert_eq!(it.next(), Some(&2));
assert_eq!(it.next(), Some(&3));
assert_eq!(it.next(), None);
}
#[test]
fn test_from_array() {
let s = Stack::from_array([1, 2, 3]);
assert_eq!(s[0], 1);
assert_eq!(s[1], 2);
assert_eq!(s[2], 3);
assert_eq!(s.len(), 3);
}
#[test]
fn test_slice_index() {
let mut s = Stack::<_, 5>::from([3, 4, 5]);
assert_eq!(s[..], [3, 4, 5]);
assert_eq!(&mut s[..], &mut [3, 4, 5]);
}
}

View File

@ -1,95 +0,0 @@
import math, random, strformat, times
import fixedseq, game, simulation, ui
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 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))
result.init
result.setState(dice, [])
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()
var r = initRand(rand(int64))
let b = newRandomGame(r)
b.display(1, 5)
echo b.showSpaces(1, 16)
let scores = b.getLegScores
echo scores.showPercents
# 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)

120
ui.nim
View File

@ -1,120 +0,0 @@
import os, math, strutils, strformat
import fixedseq, game, simulation
const help =
"""cup - Probability calculator for the board game CamelUp
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
"""
# =============================
# User input parsing/validation
# =============================
type
CmdConfig* = object
state*: seq[tuple[c: Color, p: int]]
interactive*: bool
diceRolled*: array[Color, bool]
proc parseColor(c: char): Color =
case c:
of 'R', 'r':
return cRed
of 'G', 'g':
return cGreen
of 'B', 'b':
return cBlue
of 'Y', 'y':
return cYellow
of 'P', 'p':
return cPurple
else:
raise newException(ValueError, "Invalid camel color specified.")
proc parseArgs*(): CmdConfig =
for p in os.commandLineParams():
if p == "-h":
echo help
quit 0
elif p == "-i":
result.interactive = true
elif result.state.len < 5:
let splat = p.split(':')
let sq = splat[0]
let square = sq.parseInt
let colors = splat[1]
for c in colors:
let color = parseColor(c)
result.state.add((color, square))
else:
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")