Compare commits
	
		
			2 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f2e0698608 | ||
| cade17a9a6 | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,2 @@
 | 
				
			|||||||
/target
 | 
					*.exe
 | 
				
			||||||
*.etl*
 | 
					profile_results.txt
 | 
				
			||||||
							
								
								
									
										90
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										90
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1,90 +0,0 @@
 | 
				
			|||||||
# 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
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								Cargo.toml
									
									
									
									
									
								
							@@ -1,17 +0,0 @@
 | 
				
			|||||||
[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
 | 
					 | 
				
			||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							@@ -1,12 +1,10 @@
 | 
				
			|||||||
# `cup` - CamelUp probability calculator
 | 
					# `cup` - CamelUp probability calculator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This tool calculates probable outcomes for the board game CamelUp. It can
 | 
					This tool calculates probable outcomes for the board game CamelUp.
 | 
				
			||||||
calculate all possible outcomes for a single game leg in about 5ms, so
 | 
					It can calculate all possible outcomes for a single game leg in about 5ms, so effectively instantaneously.
 | 
				
			||||||
effectively instantaneously. Full-game calculations take a little bit longer
 | 
					Full-game calculations take a little bit longer and are not exact (since it isn't practical to simulate all possible full game states.)
 | 
				
			||||||
and are not exact (since it isn't practical to simulate all possible full
 | 
					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%.
 | 
				
			||||||
game states.) However it can easily simulate a million random games in about
 | 
					(Numbers from running on a Ryzen 3700X.)
 | 
				
			||||||
80ms in the worst case, which should provide estimates accurate to within
 | 
					 | 
				
			||||||
about 0.2%. (Numbers from running on a Ryzen 3700X.)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
Usage: 
 | 
					Usage: 
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										96
									
								
								combinators.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								combinators.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					import algorithm, random, sugar
 | 
				
			||||||
 | 
					import fastrand, 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.fastRand(1..3)))
 | 
				
			||||||
							
								
								
									
										5
									
								
								config.nims
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								config.nims
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					--threads: on
 | 
				
			||||||
 | 
					--d: release
 | 
				
			||||||
 | 
					--opt: speed
 | 
				
			||||||
 | 
					--passC: -flto
 | 
				
			||||||
 | 
					--passL: -flto
 | 
				
			||||||
							
								
								
									
										17
									
								
								cup.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								cup.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					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()
 | 
				
			||||||
							
								
								
									
										74
									
								
								fastrand.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								fastrand.nim
									
									
									
									
									
										Normal 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)
 | 
				
			||||||
							
								
								
									
										146
									
								
								fixedseq.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								fixedseq.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
				
			|||||||
 | 
					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
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								game.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,161 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
							
								
								
									
										138
									
								
								simulation.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								simulation.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
				
			|||||||
 | 
					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
									
									
									
									
									
								
							
							
						
						
									
										375
									
								
								src/game.rs
									
									
									
									
									
								
							@@ -1,375 +0,0 @@
 | 
				
			|||||||
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
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								src/main.rs
									
									
									
									
									
								
							@@ -1,40 +0,0 @@
 | 
				
			|||||||
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
									
									
									
									
									
								
							
							
						
						
									
										265
									
								
								src/stack.rs
									
									
									
									
									
								
							@@ -1,265 +0,0 @@
 | 
				
			|||||||
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]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										97
									
								
								test.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								test.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					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]] = 
 | 
				
			||||||
 | 
					  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 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 =
 | 
				
			||||||
 | 
					  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:
 | 
				
			||||||
 | 
					  games(10, 10_000_000).summarize()
 | 
				
			||||||
							
								
								
									
										120
									
								
								ui.nim
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								ui.nim
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,120 @@
 | 
				
			|||||||
 | 
					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")
 | 
				
			||||||
		Reference in New Issue
	
	Block a user