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..e16e568 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cup" +version = "0.1.0" +dependencies = [ + "enum-map", +] + +[[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 = "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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3ca664a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[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" 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..92b7fc1 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,191 @@ +use enum_map::{Enum, EnumMap}; + +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([ +// 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)] +pub struct Game { + squares: [Square; 16], + dice: EnumMap, + camels: EnumMap, +} + +impl Game { + fn new() -> Self { + Self::default() + } + + fn set_state(&mut self, squares: [Square; 16], dice: EnumMap) { + self.squares = squares; + self.dice = dice; + for (i, square) in self.squares.iter().enumerate() { + if let Square::Camels(stack) = square { + for camel in stack.iter() { + self.camels[*camel] = i + } + } + } + } + + fn update_positions(&mut self, target_sq: usize) { + match self.squares[target_sq] { + Square::Camels(stack) => { + for camel in stack.iter() { + self.camels[*camel] = target_sq; + } + } + _ => () + } + } + + // returns winner if there is one + 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(); + + 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]] = src_sq + dst_rel_idx + 1; + } + } + 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]] = src_sq + dst_rel_idx + 1; + } + } + + self.update_positions(dst_rel_idx); + } + + self.dice[die] = true; + None + } +} + + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_advance() { + use Color::*; + let mut game = Game::new(); + // all dice are false (not rolled) to start with + assert_eq!(game.dice.values().any(|&v| v), false); + + let mut squares = [Square::Camels(Default::default()); 16]; + let one = squares[0].assume_stack_mut(); + one.push(Blue); + one.push(Yellow); + squares[1].assume_stack_mut().push(Red); + let three = squares[2].assume_stack_mut(); + three.push(Green); + three.push(Purple); + // BY, R, GP + + game.set_state(squares, Default::default()); + + game.advance(Yellow, 2); + println!("{:?}", game.camels); + 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, + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..79a05f3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,7 @@ +mod stack; +mod game; + + +fn main() { + println!("Hello, world!"); +} diff --git a/src/stack.rs b/src/stack.rs new file mode 100644 index 0000000..5a61d59 --- /dev/null +++ b/src/stack.rs @@ -0,0 +1,226 @@ +use std::ops::Index; +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) + } +} + + +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 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); + } +} 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")