create tidepool crate, queue/garbage sim infrastructure

This commit is contained in:
tali 2023-04-10 14:13:32 -04:00
parent a40d336911
commit c0a3f318b1
7 changed files with 511 additions and 0 deletions

54
Cargo.lock generated
View File

@ -267,6 +267,12 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
@ -309,6 +315,45 @@ 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 = "rand_xoshiro"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
dependencies = [
"rand_core",
]
[[package]]
name = "regex"
version = "1.7.1"
@ -435,6 +480,15 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tidepool"
version = "0.1.0"
dependencies = [
"mino",
"rand",
"rand_xoshiro",
]
[[package]]
name = "tracing"
version = "0.1.37"

View File

@ -3,4 +3,5 @@ members = [
"mino",
"mino-code-gen",
"fish",
"tidepool",
]

10
tidepool/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "tidepool"
description = "Tools for automated testing of Blockfish on cheese race"
version = "0.1.0"
edition = "2021"
[dependencies]
mino = { path = "../mino" }
rand = "0.8"
rand_xoshiro = "0.6"

181
tidepool/src/garbage.rs Normal file
View File

@ -0,0 +1,181 @@
use mino::matrix::{MatBuf, COLUMNS};
use rand::Rng;
use std::ops::RangeInclusive;
/// Manages the current level of garbage and the remaining garbage lines.
pub struct Garbage<R: Rng> {
cheese: std::iter::Take<Cheese<R>>,
// current level of garbage on the matrix
level: i16,
// min/max garbage to insert on the matrix
range: RangeInclusive<i16>,
}
impl<R: Rng> Garbage<R> {
/// Constructs a new garbage generator.
///
/// - `rng`: random number source
/// - `count`: total number of garbage rows to insert
/// - `range`: min/max amount of garbage on the matrix at a given time
pub fn new(rng: R, count: usize, range: RangeInclusive<i16>) -> Self {
Self {
cheese: Cheese::new(rng).take(count),
level: 0,
range,
}
}
/// Signals that a line clear happened at row `min_y`. This is necessary to determine
/// how much garbage needs to be inserted. Returns the number of garbage lines that
/// were removed from this line clear.
pub fn clear(&mut self, min_y: i16) -> usize {
if min_y < self.level {
let removed = (self.level - min_y) as usize;
self.level = min_y;
removed
} else {
0
}
}
/// Inserts new garbage lines to the bottom of `mat`. If `comboing` is `true` then
/// less garbage lines are inserted (like jstris's cheese race mechanics). Returns the
/// number of new garbage lines inserted.
pub fn insert(&mut self, mat: &mut MatBuf, comboing: bool) -> usize {
let target = if comboing {
*self.range.start()
} else {
*self.range.end()
};
let difference = (target as usize).saturating_sub(self.level as usize);
(&mut self.cheese)
.take(difference)
.map(|col| {
mat.shift_up();
mat.fill_row(0, garbage_row(col));
self.level += 1;
})
.count()
}
}
fn garbage_row(col: i16) -> u16 {
!(1 << col)
}
struct Cheese<R: Rng> {
rng: R,
col: i16,
}
impl<R: Rng> Cheese<R> {
fn new(mut rng: R) -> Self {
let col = rng.gen_range(0..COLUMNS);
Self { rng, col }
}
}
impl<R: Rng> Iterator for Cheese<R> {
type Item = i16;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
self.col += self.rng.gen_range(1..COLUMNS);
self.col %= COLUMNS;
Some(self.col)
}
}
#[cfg(test)]
mod tests {
use super::*;
use mino::mat;
#[test]
fn test_garbage_row() {
let mat = mat! {
"xxxxxxxxx.";
"xxxxx.xxxx";
"xx.xxxxxxx";
".xxxxxxxxx";
};
assert_eq!(mat[0], garbage_row(0));
assert_eq!(mat[1], garbage_row(2));
assert_eq!(mat[2], garbage_row(5));
assert_eq!(mat[3], garbage_row(9));
}
#[test]
fn test_cheese_no_duplicates() {
for _ in 0..50 {
let cheese = Cheese::new(rand::thread_rng());
let mut prev = None;
let mut history = std::collections::HashSet::new();
for x1 in cheese.take(500) {
if let Some(x0) = prev {
assert_ne!(x0, x1);
}
prev = Some(x1);
history.insert(x1);
}
assert_eq!(history.len(), COLUMNS as usize);
for x in 0..COLUMNS {
assert!(history.contains(&x));
}
}
}
#[test]
fn test_garbage_insert() {
let mut mat = MatBuf::new();
let mut garbage_left = 15;
let mut downstacked = 0;
let mut garbage = Garbage::new(rand::thread_rng(), garbage_left, 3..=9);
garbage_left -= garbage.insert(&mut mat, true);
assert_eq!(garbage_left, 12);
assert_eq!(mat.rows(), 3);
let m0 = mat[0];
let m1 = mat[1];
let m2 = mat[2];
garbage_left -= garbage.insert(&mut mat, false);
assert_eq!(garbage_left, 6);
assert_eq!(mat.rows(), 9);
assert_ne!(mat[5], m0);
assert_eq!(mat[6], m0);
assert_eq!(mat[7], m1);
assert_eq!(mat[8], m2);
mat.fill_row(9, mino::matrix::EMPTY_ROW | 0b1);
assert_eq!(garbage.insert(&mut mat, false), 0);
assert_eq!(mat.rows(), 10);
mat.fill_row(7, mino::matrix::FULL_ROW);
mat.fill_row(8, mino::matrix::FULL_ROW);
assert_eq!(mat.clear_lines(), 7..9);
assert_eq!(mat.rows(), 8);
downstacked += garbage.clear(7);
assert_eq!(downstacked, 2);
garbage_left -= garbage.insert(&mut mat, false);
assert_eq!(garbage_left, 4);
assert_eq!(mat.rows(), 10);
mat.clear();
downstacked += garbage.clear(0);
assert_eq!(downstacked, 11);
garbage_left -= garbage.insert(&mut mat, false);
assert_eq!(garbage_left, 0);
assert_eq!(mat.rows(), 4);
mat.clear();
downstacked += garbage.clear(0);
assert_eq!(downstacked, 15);
assert_eq!(garbage.insert(&mut mat, false), 0);
assert_eq!(mat.rows(), 0);
}
}

3
tidepool/src/lib.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod garbage;
pub mod queue;
pub mod sim;

185
tidepool/src/queue.rs Normal file
View File

@ -0,0 +1,185 @@
use mino::srs::PieceType;
use rand::Rng;
use std::collections::VecDeque;
/// Manages the preview pieces and hold slot, and automatically refills the previews
/// whenever pieces are consumed from the front of the queue.
pub struct Queue<R: Rng> {
hold: Option<PieceType>,
next: VecDeque<PieceType>,
bag: Bag<R, PieceType>,
}
static PIECES: [PieceType; 7] = [
// this order is arbitrary (alphabetical), but must be set in stone in order for bags
// to be reproducible by identical PRNG's.
PieceType::I,
PieceType::J,
PieceType::L,
PieceType::O,
PieceType::S,
PieceType::T,
PieceType::Z,
];
impl<R: Rng> Queue<R> {
/// Constructs a new queue.
///
/// - `rng`: random number source
/// - `count`: number of next pieces.
pub fn new(rng: R, count: usize) -> Self {
assert!(count > 0, "next pieces cannot be empty");
let mut next = VecDeque::with_capacity(count);
let mut bag = Bag::new(rng, PIECES);
while next.len() < count {
next.push_back(bag.pop());
}
Self {
hold: None,
next,
bag,
}
}
/// Returns the current piece in the hold slot.
pub fn hold(&self) -> Option<PieceType> {
self.hold
}
/// Returns the list of the next previews.
pub fn next(&self) -> Vec<PieceType> {
self.next.iter().copied().collect()
}
/// Remove a piece from the front of the queue. The piece must either by the current
/// piece at the front of the next-previews, or it must be reachable by swapping with
/// the piece in hold; if the hold is empty then the second piece in the queue is
/// reachable by moving the first piece into the hold slot.
///
/// Panics if the given piece is not reachable from this queue.
pub fn remove(&mut self, ty: PieceType) {
// remove current piece from front of queue
let top = self.pop_front();
if top != ty {
// if not placing current piece, then must be placing either the hold piece or
// the piece reachable by hold (if hold was previously empty).
if let Some(held) = self.hold.replace(top) {
// hold piece
assert_eq!(ty, held);
} else {
// hold empty, so get next reachable piece
assert_eq!(ty, self.pop_front());
}
}
}
fn pop_front(&mut self) -> PieceType {
let ty = self.next.pop_front().expect("len > 0");
self.next.push_back(self.bag.pop());
ty
}
}
struct Bag<R: Rng, T: Copy> {
rng: R,
bag: Vec<T>,
count: usize,
}
impl<R: Rng, T: Copy> Bag<R, T> {
fn new(rng: R, init: impl IntoIterator<Item = T>) -> Self {
let bag = init.into_iter().collect::<Vec<_>>();
let count = bag.len();
assert!(count > 0, "empty init bag");
Self { rng, bag, count }
}
fn pop(&mut self) -> T {
let i = self.count - 1;
self.bag.swap(self.rng.gen_range(0..self.count), i);
self.count = if i == 0 { self.bag.len() } else { i };
self.bag[i]
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_bag_order() {
let mut tys = std::collections::HashSet::new();
let mut bag = Bag::new(rand::thread_rng(), 'A'..='Z');
for _ in 0..50 {
tys.extend('A'..='Z');
for _ in 0..26 {
let ch = bag.pop();
assert!(tys.remove(&ch), "{ch:?}");
}
assert!(tys.is_empty());
}
}
#[test]
fn test_pop() {
let mut que = Queue::new(rand::thread_rng(), 5);
assert_eq!(que.hold(), None);
let next = que.next();
assert_eq!(next.len(), 5);
// [](a)bcde
let a = next[0];
let b = next[1];
let c = next[2];
let d = next[3];
let e = next[4];
assert_ne!(a, b);
assert_ne!(c, d);
assert_ne!(a, e);
// [](b)cdef -> a
que.remove(a);
let next = que.next();
assert_eq!(que.hold, None);
assert_eq!(next.len(), 5);
assert_eq!(next[0], b);
assert_eq!(next[1], c);
assert_eq!(next[2], d);
assert_eq!(next[3], e);
let f = next[4];
// [b](d)efgh -> c
que.remove(c);
assert_eq!(que.hold(), Some(b));
let next = que.next();
assert_eq!(next.len(), 5);
assert_eq!(next[0], d);
assert_eq!(next[1], e);
assert_eq!(next[2], f);
let g = next[3];
let h = next[4];
// [b](e)fghi -> d
que.remove(d);
assert_eq!(que.hold(), Some(b));
let next = que.next();
assert_eq!(next.len(), 5);
assert_eq!(next[0], e);
assert_eq!(next[1], f);
assert_eq!(next[2], g);
assert_eq!(next[3], h);
let i = next[4];
// [e](f)ghij -> b
que.remove(b);
assert_eq!(que.hold(), Some(e));
let next = que.next();
assert_eq!(next.len(), 5);
assert_eq!(next[0], f);
assert_eq!(next[1], g);
assert_eq!(next[2], h);
assert_eq!(next[3], i);
//let j = next[4];
}
}

77
tidepool/src/sim.rs Normal file
View File

@ -0,0 +1,77 @@
use crate::garbage::Garbage;
use crate::queue::Queue;
use mino::matrix::{Mat, MatBuf};
use mino::srs::{Piece, PieceType};
use rand::SeedableRng as _;
use std::ops::RangeInclusive;
pub struct Options {
/// Total number of garbage lines required to clear.
pub goal: usize,
/// Min/max number of garbage lines on the matrix at a given time.
pub garbage: RangeInclusive<i16>,
/// Number of preview pieces.
pub previews: usize,
}
impl Options {
pub const fn jstris(goal: usize) -> Self {
Self {
goal,
garbage: 3..=9,
previews: 5,
}
}
}
type Rng = rand_xoshiro::Xoshiro256StarStar;
pub struct Simul {
queue: Queue<Rng>,
garbage: Garbage<Rng>,
matrix: MatBuf,
lines_left: usize,
}
impl Simul {
pub fn new(seed: [u8; 32], options: Options) -> Self {
let rng1 = Rng::from_seed(seed);
let mut rng2 = rng1.clone();
rng2.jump();
let queue = Queue::new(rng1, options.previews);
let mut garbage = Garbage::new(rng2, options.goal, options.garbage);
let mut matrix = MatBuf::new();
garbage.insert(&mut matrix, false);
Self {
queue,
garbage,
matrix,
lines_left: options.goal,
}
}
pub fn play(&mut self, piece: Piece) {
// play piece
self.queue.remove(piece.ty);
piece.cells().fill(&mut self.matrix);
// process line clears
let c = self.matrix.clear_lines();
self.lines_left -= self.garbage.clear(c.start);
self.garbage.insert(&mut self.matrix, !c.is_empty());
}
pub fn matrix(&self) -> &Mat {
&self.matrix
}
pub fn queue(&self) -> (Option<PieceType>, Vec<PieceType>) {
(self.queue.hold(), self.queue.next())
}
pub fn lines_left(&self) -> usize {
self.lines_left
}
}