From 73f70e4fa90ff23f09fc9433af2e7341fdcb60c4 Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Mon, 2 Jan 2023 10:34:30 -0800 Subject: [PATCH] RIIR --- .gitignore | 4 +- Cargo.lock | 131 +++++++++++++++++ Cargo.toml | 10 ++ combinators.nim | 88 ----------- cup.nim | 16 -- cup.nims | 4 - fixedseq.nim | 149 ------------------- game.nim | 172 ---------------------- simulation.nim | 151 ------------------- src/game.rs | 380 ++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 26 ++++ src/stack.rs | 265 +++++++++++++++++++++++++++++++++ test.nim | 125 ---------------- test.nims | 3 - ui.nim | 112 -------------- 15 files changed, 813 insertions(+), 823 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml delete mode 100644 combinators.nim delete mode 100644 cup.nim delete mode 100644 cup.nims delete mode 100644 fixedseq.nim delete mode 100644 game.nim delete mode 100644 simulation.nim create mode 100644 src/game.rs create mode 100644 src/main.rs create mode 100644 src/stack.rs delete mode 100644 test.nim delete mode 100644 test.nims delete mode 100644 ui.nim diff --git a/.gitignore b/.gitignore index 1756b4b..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -*.exe -profile_results.txt -callgrind.out.* \ No newline at end of file +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..92bfa5d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,131 @@ +# 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", + "rand", +] + +[[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 = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[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 = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[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" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..04d0d45 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[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" +"rand" = "0.8.5" diff --git a/combinators.nim b/combinators.nim deleted file mode 100644 index e86fc65..0000000 --- a/combinators.nim +++ /dev/null @@ -1,88 +0,0 @@ -import algorithm -import fixedseq, game - - -proc nextPermutation(x: var FixedSeq): bool = - # copied shamelessly from std/algorithm.nim - if x.len < 2: - return false - - var i = x.high - while i > 0 and x[i - 1] >= x[i]: - dec i - - if i == 0: - return false - - var j = x.high - while j >= i and x[j] <= x[i - 1]: - dec j - - swap x[j], x[i - 1] - x.reverse(i, x.high) - - result = true - - -proc prevPermutation(x: var FixedSeq): bool = - # copied shamelessly from std/algorithm.nim - if x.len < 2: - return false - - var i = x.high - while i > 0 and x[i - 1] <= x[i]: - dec i - - if i == 0: - return false - - x.reverse(i, x.high) - - var j = x.high - while j >= i and x[j - 1] < x[i - 1]: - dec j - - swap x[i - 1], x[j] - - result = true - - -iterator allPermutations*(x: FixedSeq): FixedSeq = - # returns all permutations of a given seq. Order is wonky but we don't care. - var workingCopy = x - yield workingCopy - while workingCopy.nextPermutation: # this mutates workingCopy - yield workingCopy - workingCopy = x - while workingCopy.prevPermutation: - yield workingCopy - - -iterator allDigits*(lo, hi, size: Natural): auto = - if size > 0: # otherwise we get an infinite loop - var digits: FixedSeq[5, int] - 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: ColorStack): 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: FixedSeq[5, Die] - for i in 0'u8 .. dice.high: - f.add((color: perm[i], value: digits[i])) - yield f diff --git a/cup.nim b/cup.nim deleted file mode 100644 index db70baf..0000000 --- a/cup.nim +++ /dev/null @@ -1,16 +0,0 @@ -import game, simulation, ui - - -when isMainModule: - let config = parseArgs() - var b: Board - b.setState(config.state) - - let legScores = b.getLegScores - let gameScores = b.randomGames(1_000_000) - - echo b.showSpaces(1, 16) - echo "\nCurrent leg probabilities:" - echo legScores.showPercents() - echo "\nFull game probabilities (1M simulations):" - echo gameScores.showPercents() diff --git a/cup.nims b/cup.nims deleted file mode 100644 index 9934587..0000000 --- a/cup.nims +++ /dev/null @@ -1,4 +0,0 @@ ---threads: on ---d: release ---d: lto ---opt: speed diff --git a/fixedseq.nim b/fixedseq.nim deleted file mode 100644 index e994cf8..0000000 --- a/fixedseq.nim +++ /dev/null @@ -1,149 +0,0 @@ -import random - - -type - FixedSeq*[Size: static range[0..255], Content] = object - data: array[Size, Content] - len*: uint8 - - -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, idx: Natural): FixedSeq.Content = - when not defined(danger): - if idx.uint8 >= s.len: - raise newException(IndexDefect, "index " & $idx & " is out of bounds.") - s.data[idx] - - -proc `[]`*(s: var FixedSeq, idx: Natural): var FixedSeq.Content = - when not defined(danger): - if idx.uint8 >= s.len: - raise newException(IndexDefect, "index " & $idx & " is out of bounds.") - s.data[idx] - - -proc `[]`*(s: FixedSeq, idx: BackwardsIndex): auto = - when not defined(danger): - if s.len == 0: - raise newException(IndexDefect, "index out of bounds, the container is empty.") # matching stdlib again - s.data[s.len - idx.uint8] - - -proc `[]=`*(s: var FixedSeq, idx: Natural, v: FixedSeq.Content) = - when not defined(danger): - if idx.uint8 >= s.len: - raise newException(IndexDefect, "index " & $idx & " is out of bounds.") - s.data[idx] = v - - -proc high*(s: FixedSeq): auto = - result = s.len - 1 - - -proc low*(s: FixedSeq): auto = - result = case s.len - of 0: 0 # a bit weird but it's how the stdlib seq works - else: s.len - 1 - - -iterator items*(s: FixedSeq): auto = - for i in 0'u8 ..< s.len: - yield s.data[i] - - -iterator asInt*(s: FixedSeq): int8 = - for i in 0'u8 ..< s.len: - 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.Content) = - s.data[s.len] = v # will raise exception if out of bounds - inc s.len - - -proc insert*(s: var FixedSeq, v: FixedSeq.Content, idx: Natural = 0) = - for i in countdown(s.len - 1, idx.uint8): - swap(s.data[i], s.data[i + 1]) # will also raise exception if out of bounds - s.data[idx] = v - inc s.len - - -proc delete*(s: var FixedSeq, idx: Natural) = - when not defined(danger): - if idx.uint8 >= s.len: - raise newException(IndexDefect, "index " & $idx & " is out of bounds.") - dec s.len - for i in idx.uint8 ..< s.len: - swap(s.data[i], s.data[i + 1]) - - -proc clear*(s: var FixedSeq) = - s.len = 0 - - -proc find*(s: FixedSeq, needle: FixedSeq.Content): int = - 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) = - when not defined(danger): - if s.len < s.data.len.uint8: - raise newException(IndexDefect, "Cannot shuffle a partially-full FixedSeq") - r.shuffle(s.data) - -proc shuffle*(s: var FixedSeq) = - when not defined(danger): - if s.len < s.data.len.uint8: - raise newException(IndexDefect, "Cannot shuffle a partially-full FixedSeq") - shuffle(s.data) - - -proc moveSubstack*(src, dst: var FixedSeq; start: Natural) = - var count = 0'u8 # have to track this separately apparently - for idx in start ..< src.len: - swap(src.data[idx], dst.data[dst.len + count]) - inc count - dst.len += count - src.len -= count - - -proc moveSubstackPre*(src, dst: var FixedSeq; start: Natural) = - let ssLen = src.len - start.uint8 # length of substack - for i in countdown(dst.len - 1, 0): - swap(dst.data[i], dst.data[i + ssLen]) - - var count = 0 - for i in start ..< src.len: - swap(src.data[i], dst.data[count]) - inc count - - dst.len += ssLen - src.len -= ssLen diff --git a/game.nim b/game.nim deleted file mode 100644 index 0ad9659..0000000 --- a/game.nim +++ /dev/null @@ -1,172 +0,0 @@ -import hashes, options -import fixedseq - - -type - Color* = enum - cRed, cGreen, cBlue, cYellow, cPurple - - ColorStack* = FixedSeq[5, Color] - -const - 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.uint8 < 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] - - GameState* = object - dice*: array[Color, bool] - camels*: FixedSeq[5, tuple[c: Color, p: range[1..16]]] - tiles*: FixedSeq[8, tuple[t: Tile, p: range[1..16]]] # max 8 players, so max 8 tiles - - Board* = object - squares*: array[1..16, Square] - camels*: array[Color, range[1..16]] - diceRolled*: array[Color, bool] - winner*: Option[Color] - gameOver*: bool - - -# use a template here for better inlining -template `[]`*[T](b: var Board, idx: T): var Square = - b.squares[idx] - -# apparently we need separate ones for mutable and non-mutable -template `[]`*[T](b: Board, idx: T): 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 leader*(b: Board): Color = - let leadSquare = max(b.camels) - result = b[leadSquare].camels[^1] - - -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; state: GameState) = - for sq in b.squares.mitems: - if sq.camels.len > 0: - sq.camels.clear() - elif sq.tile.isSome: - sq.tile = none[Tile]() - - for (color, dest) in state.camels: # note that `camels` is ordered, as this determines stacking - b[dest].camels.add(color) - b.camels[color] = dest - - for (tile, dest) in state.tiles: - b[dest].tile = some(tile) - - b.diceRolled = state.dice - - -proc getState*(b: Board): GameState = - var camelCount = 0 - let start = min(b.camels) - for pos in start .. b.squares.high: - let sq = b[pos] - for color in sq.camels: - result.camels.add((c: color, p: pos)) - camelCount += 1 - - if sq.tile.isSome: - result.tiles.add((t: sq.tile.get, p: pos)) - if camelCount >= 5: - break - - result.dice = b.diceRolled - - -proc diceRemaining*(b: Board): ColorStack = - for color, isRolled in b.diceRolled: - if not isRolled: result.add(color) - - -proc resetDice*(b: var Board) = - for c in Color: - 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.winner = 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 = cast[uint8](b[startPos].camels.find(color)) # cast is safe here, as long as b.camels is valid - if prepend: - b[startPos].camels.moveSubstackPre(b[endPos].camels, stackStart) - let stackLen = b[startPos].camels.len - stackStart - for i in 0'u8 ..< 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 - - b.diceRolled[color] = true \ No newline at end of file diff --git a/simulation.nim b/simulation.nim deleted file mode 100644 index aabd709..0000000 --- a/simulation.nim +++ /dev/null @@ -1,151 +0,0 @@ -import cpuinfo, math, options, random, sequtils, 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 - for i, c in b.diceRolled: - if not c: diceRemaining.add(i) - - let origState = b.getState - var prediction = b - for future in possibleFutures(diceRemaining): - # var prediction = b # make a copy so we can mutate - for dieRoll in future: - prediction.advance(dieRoll) - yield prediction - prediction.setState(origState) - - -proc getLegScores*(b: Board): ScoreSet = - # special case if all dice have been rolled - if allIt(b.diceRolled, it): - inc result[b.leader] - return result - - for prediction in b.legEndStates: - inc result[prediction.leader] - - -# ===================== -# Full-game simulations -# ===================== - -proc randomGame*(b: Board, r: var Rand): Color = - var projection = b - var dice = projection.diceRemaining - - while true: - dice.shuffle(r) - for color in dice: - projection.advance((color, r.rand(1..3))) - if projection.gameOver: - return projection.winner.get - # if we started with <5 dice, we need to reset for the next full leg - if dice.len < 5: - projection.resetDice() - dice = projection.diceRemaining - - -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 \ No newline at end of file diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..e4b4851 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,380 @@ +use enum_map::{Enum, EnumMap}; +use rand::{ + Rng, + seq::SliceRandom, + distributions::{Distribution, Uniform}, +}; + +use crate::stack::Stack; + + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Default, Enum)] +pub enum Color { + #[default] Red, Green, Blue, Yellow, Purple, +} + +// const COLORS: Stack = 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; + + +#[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, + camels: EnumMap, +} + +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 mut rng = rand::thread_rng(); + let mut dice = *&COLORS; + dice.shuffle(&mut rng); + for color in dice { + let roll = rng.gen_range(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) { + 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) { + 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 { + 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) -> Option { + let mut rng = rand::thread_rng(); + let mut leg_dice: Stack = Stack::new(); + for (color, rolled) in self.dice { + if !rolled { + leg_dice.push(color); + } + } + + (&mut leg_dice[..]).shuffle(&mut rng); + for color in leg_dice.iter() { + let roll = rng.gen_range(1..=3); + if let Some(winner) = self.advance(*color, roll) { + return Some(winner); + } + } + None + } + + fn finish_game_random(&mut self) -> Color { + if let Some(winner) = self.finish_leg_random() { + return winner; + } + + // we are now guaranteed to be at the start of a new leg, + // so we don't need to check the dice state + let mut rng = rand::thread_rng(); + let roll_dist = Uniform::from(1..=3); + let mut dice = COLORS; // makes a copy of the constant + + loop { + // easiest if we shuffle at the start of the leg + dice.shuffle(&mut rng); + for i in 0..5 { + let roll = roll_dist.sample(&mut rng); + if let Some(winner) = self.advance(dice[i], roll) { + return winner; + } + } + } + } + + pub fn project_outcomes(&self, count: usize) -> EnumMap { + let (orig_camels, orig_dice) = self.get_state(); + let mut projection = *self; + + let mut scores: EnumMap = EnumMap::default(); + for i in 0..count { + let winner = projection.finish_game_random(); + 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])); + projection.finish_leg_random(); + // 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 + assert!(matches!(game.finish_leg_random(), 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); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e1ab2d4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +use std::time::Instant; + +mod stack; +mod game; + +use game::Game; + + +fn main() { + let n_games = 10_000_000; + + let start = Instant::now(); + let game = Game::new_random(); + let _scores = game.project_outcomes(n_games); + let end = Instant::now(); + + let elapsed = end.duration_since(start); + let secs = elapsed.as_secs(); + let hundredths = elapsed.subsec_millis() / 10; // technically not accurate but good enough for now + + let rate = (10_000_000 as f64) / elapsed.as_secs_f64(); + + println!("Test completed:"); + println!("{n_games} in {secs}.{hundredths:02} seconds", ); + println!("Games per second: {rate}"); +} diff --git a/src/stack.rs b/src/stack.rs new file mode 100644 index 0000000..0cf5816 --- /dev/null +++ b/src/stack.rs @@ -0,0 +1,265 @@ +use std::ops::{Index, IndexMut, RangeFull}; +use std::iter::IntoIterator; + + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Stack { + data: [T; S], + len: usize // we can experiment with using u8 some other time +} + + +impl Stack { + 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 { + 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 Stack +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 Default for Stack +where T: Copy + Default +{ + fn default() -> Self { + Self::new() + } +} + + +impl Index for Stack { + type Output = T; + fn index(&self, index: usize) -> &T { + &self.data[index] + } +} + +impl Index for Stack { + type Output = [T]; + fn index(&self, _index: RangeFull) -> &[T] { + &self.data[..self.len] + } +} + +impl IndexMut for Stack { + fn index_mut(&mut self, _index: RangeFull) -> &mut [T] { + &mut self.data[..self.len] + } +} + + +impl From for Stack +where + T: Copy + Default, + I: IntoIterator +{ + 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 = 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 = Stack::new(); + let mut b: Stack = 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 = Stack::new(); + let mut b: Stack = 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 = 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]); + } +} diff --git a/test.nim b/test.nim deleted file mode 100644 index 4b442cc..0000000 --- a/test.nim +++ /dev/null @@ -1,125 +0,0 @@ -import math, random, strformat, strutils, times, std/monotimes -import cligen -import fixedseq, game, simulation - - -type - TestResults = object - scores: ScoreSet - time: Duration - - -proc ops(tr: TestResults): int = - result = sum(tr.scores) - - -proc formatNum(n: SomeNumber, decimals = 0): string = - when n is SomeFloat: - let s = $n.round(decimals) - else: - let s = $n - var parts = s.split('.') - result = parts[0].insertSep(',') - if decimals > 0: - result = result & '.' & parts[1] - - -proc summarize(tr: TestResults, opname = "operations") = - let secs = tr.time.inMilliseconds.float / 1000 - stdout.write("Test completed:\n") - stdout.write(&" {tr.ops.formatNum} {opname} in {secs.formatNum(2)} seconds\n") - stdout.write(&" {(tr.ops.float / secs).formatNum} {opname} per second\n") - stdout.flushFile() - - -proc newRandomGame(): Board = - randomize() - - var state: GameState - for i in 0 .. 4: - let pos = rand(1..3) - state.camels.add((c: Color(i), p: pos)) - state.camels.shuffle() - - result.setState(state) - - -template executionTime(body: untyped): Duration = - let start = getMonoTime() - body - getMonoTime() - start - - -# template runTest(loops: Natural, opname = "operations", body: ScoreSet): TestResults = -# var res: TestResults -# for i in 1 .. loops: -# let start = getMonoTime() -# let s = body -# res.time += (getMonoTime() - start) -# res.scores.update(s) -# res.summarize(opname) - - -# proc games(runs, samples: Natural, parallel = true) = -# let b = newRandomGame() -# runTest(runs, "games"): -# b.randomGames(samples, parallel = parallel) - - -# proc legs(runs: Natural) = -# let b = newRandomGame() -# runTest(runs, "legs"): -# b.getLegScores - - -proc games(runs, samples: Natural, parallel = true) = - var res: TestResults - for i in 1 .. runs: - let b = newRandomGame() - let dur = executionTime: - let s = b.randomGames(samples, parallel = parallel) - res.scores.update(s) - res.time += dur - res.summarize("games") - - -proc legs(runs: Natural) = - var res: TestResults - for i in 1 .. runs: - let b = newRandomGame() - let dur = executionTime: - let s = b.getLegScores - res.scores.update(s) - res.time += dur - res.summarize("legs") - - -proc spread(runs, samples: Natural) = - let b = newRandomGame() - let spread = randomSpread(b, runs, samples) - - 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() - - -const help_runs = "Number of times to run the test" -const help_samples = "Number of iterations per run" -const help_parallel = "Run test in parallel or single-threaded (default parallel)" - -proc bench() = - dispatchMulti( - [games, help = {"runs": help_runs, "samples": help_samples, "parallel": help_parallel}], - [legs, help = {"runs": help_runs}], - [spread, help = {"runs": help_runs, "samples": help_samples}] - ) - - -when isMainModule: - bench() diff --git a/test.nims b/test.nims deleted file mode 100644 index fe599ae..0000000 --- a/test.nims +++ /dev/null @@ -1,3 +0,0 @@ ---threads: on ---d: danger ---d: lto diff --git a/ui.nim b/ui.nim deleted file mode 100644 index 47807d0..0000000 --- a/ui.nim +++ /dev/null @@ -1,112 +0,0 @@ -import os, math, strutils, strformat -import fixedseq, game, simulation - - -const help = block: - # can't use regex, fortunately we are looking for a straightforward separator - let readme = slurp("./README.md") - let endPos = rfind(readme, "```") - 1 - let startPos = rfind(readme, "```", last = endPos) + 4 - readme[startPos..endPos] - - -# ============================= -# User input parsing/validation -# ============================= - -type - CmdConfig* = object - state*: GameState - 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: " & c) - - -proc parseArgs*(): CmdConfig = - for p in os.commandLineParams(): - if p == "-h": - echo help - quit 0 - elif p == "-i": - result.interactive = true - elif result.state.camels.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.camels.add((c: color, p: square)) - else: - for c in p: - let color = parseColor(c) - result.state.dice[color] = true - - if result.state.camels.len != 5: - raise newException(ValueError, "Please specify positions for all camels.") - - -# ========================== -# 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: - var bar = repeat(" ", 20) - let percentage = round(pct * 100, 2) - # populate the progress bar - let barFill = int(round(pct * 20)) - for i in 0 ..< barFill: - bar[i] = '=' - - lines[int(color)] = fmt"{color:>7}: [{bar}] {percentage:5.2f}%" - result = lines.join("\n")