This an entry for a seven-hour roguelike competition. (Some work has been done to it since the end of the competition.)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

494 lines
13 KiB

use std::ops::Div;
use rogue_util::coord::{R2i, V2i};
use rogue_util::grid::{Grid};
use rogue_util::raster;
use super::{Tile, Content};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use rand_distr::{Poisson, Distribution, weighted::WeightedIndex, Uniform, Bernoulli};
use std::hash::{Hash, Hasher};
use std::collections::hash_map::DefaultHasher;
const GRID_SIZE: isize = 32;
const AVG_ROOMS_PER_CELL: f64 = 2.0;
// room types: [rectangle, circle]
const ROOM_TYPE_WEIGHTS: [usize; 2] = [1, 1];
const RECT_MIN_SIZE: isize = 3;
const RECT_MAX_SIZE: isize = 10;
const CIRC_MIN_RAD: isize = 2;
const CIRC_MAX_RAD: isize = 7;
// path strategies: [no path, Bresenham line between centers, orthogonal paths, random paths nearby]
const PATH_STRATEGY_WEIGHTS: [usize; 4] = [2500, 5, 100, 100];
const USE_RECTANGLE_CENTER_FOR_PATH_END_PROB: f64 = 0.5;
const PATH_ORTHOG_AVG_NUM_STOPS_PER_LEN: f64 = 0.5;
const PATH_CIRC_AVG_NUM_STOPS_PER_LEN: f64 = 0.3;
pub mod extra_utils
{
use rogue_util::coord::{R2i, V2i};
use rand_distr::{Uniform, Distribution};
use rand::Rng;
pub fn rects_have_intersection_or_common_edge(r1: R2i, r2: R2i) -> bool
{
let enlarged = r1.grow(1);
enlarged.intersect(r2).is_some()
}
pub fn get_rect_edge_by_index(r: R2i, i: isize) -> V2i
{
let orig = r.origin();
let V2i(xd, yd) = r.dim();
match i
{
i if i < 0 => panic!("index out of bounds when finding a rectangle edge"),
i if i < xd - 1 => orig + V2i(i, 0),
i if i < 2 * xd - 2 => orig + V2i(i - xd + 2, yd - 1),
i if i < 2 * xd + yd - 3 => orig + V2i(0, i - 2 * xd + 3),
i if i < 2 * xd + 2 * yd - 4 => orig + V2i(xd - 1, i - 2 * xd - yd + 3),
_ => panic!("index out of bounds when finding a rectangle edge"),
}
}
pub fn gen_rand_in_rect<R: Rng>(r: &R2i, rg: &mut R) -> V2i
{
let orig = r.origin();
let dims = r.dim();
V2i(Uniform::new(orig.0, orig.0 + dims.0).sample(rg),
Uniform::new(orig.1, orig.1 + dims.1).sample(rg))
}
}
use extra_utils::*;
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash)]
enum RoomShape
{
Rect(R2i),
Circ(V2i, isize),
}
impl RoomShape
{
fn offset_grid(self, grid_offset: V2i) -> RoomShape
{
let offset = grid_offset * V2i(GRID_SIZE, GRID_SIZE);
match self
{
Self::Rect(rect) => Self::Rect(rect.translate(offset)),
Self::Circ(center, radius) => Self::Circ(center + offset, radius),
}
}
fn get_bounding_rect(&self) -> R2i
{
match self
{
Self::Rect(rect) => *rect,
Self::Circ(center, radius) => R2i::origin_dim(*center - V2i(radius - 1, radius - 1), V2i(2 * radius, 2 * radius)),
}
}
fn into_unique_form(&self, grid_coords: V2i) -> RoomShape
{
let coords = grid_coords * V2i(GRID_SIZE, GRID_SIZE);
match self
{
Self::Rect(rect) => Self::Rect(rect.translate(coords)),
Self::Circ(c, r) => Self::Circ(*c + coords, *r),
}
}
fn origin(&self) -> V2i
{
match self
{
Self::Rect(rect) => rect.origin(),
Self::Circ(c, _) => *c,
}
}
fn gen_rand_in<R: Rng>(&self, rg: &mut R) -> V2i
{
match self
{
Self::Rect(rect) => gen_rand_in_rect(rect, rg),
Self::Circ(c, r) =>
{
let bbox = self.get_bounding_rect();
let r2 = r * r;
loop
{
let p = gen_rand_in_rect(&bbox, rg);
if (p - *c).l2_sq() < r2
{
break p;
}
}
},
}
}
}
enum Path
{
Piecewise(Vec<Path>),
Line(V2i, V2i),
}
fn get_random_point<T: rand::Rng>(rng: &mut T) -> V2i
{
let coorddist = Uniform::new(0, GRID_SIZE);
let x = coorddist.sample(rng);
let y = coorddist.sample(rng);
V2i(x, y)
}
fn get_room_shapes(seed: u64, coords: V2i) -> Vec<RoomShape>
{
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
coords.hash(&mut hasher);
let mut rg = SmallRng::seed_from_u64(hasher.finish());
let nrooms: u64 = Poisson::new(AVG_ROOMS_PER_CELL).unwrap().sample(&mut rg);
let mut rooms = Vec::new();
let chooser = WeightedIndex::new(&ROOM_TYPE_WEIGHTS).unwrap();
let rect_size_dist = Uniform::new_inclusive(RECT_MIN_SIZE, RECT_MAX_SIZE);
let circ_rad_dist = Uniform::new_inclusive(CIRC_MIN_RAD, CIRC_MAX_RAD);
for _ii in 0 .. nrooms
{
let shape = match chooser.sample(&mut rg)
{
0 =>
{
RoomShape::Rect(R2i::origin_dim(get_random_point(&mut rg), V2i(rect_size_dist.sample(&mut rg), rect_size_dist.sample(&mut rg))))
},
1 =>
{
RoomShape::Circ(get_random_point(&mut rg), circ_rad_dist.sample(&mut rg))
},
_ => panic!("random index out of range in worldgen (room generation - unknown room type)"),
};
rooms.push(shape);
}
rooms
}
fn get_nearby_room_shapes(seed: u64, coords: V2i, radius: isize) -> impl Iterator<Item=RoomShape>
{
R2i::origin_opp(V2i(-radius, -radius), V2i(1 + radius, 1 + radius)).iter()
.map(move |v| { get_room_shapes(seed, v + coords).into_iter()
.map(move |shape| shape.offset_grid(v)) }).flatten()
}
fn carve_rooms<T: Iterator<Item=RoomShape>>(map: &mut [Tile], shapes: T)
{
let gbox = R2i::origin_dim(V2i(0, 0), V2i(GRID_SIZE, GRID_SIZE));
for shape in shapes
{
let bbox = shape.get_bounding_rect().intersect(gbox);
if let Some(bbox) = bbox
{
match shape
{
RoomShape::Rect(_) =>
{
for tile in bbox.iter()
{ map[(tile.1 * GRID_SIZE + tile.0) as usize].content = Content::Air; }
},
RoomShape::Circ(c, r) =>
{
for tile in bbox.iter()
{
if (tile - c).l2_sq() < r * r {
map[(tile.1 * GRID_SIZE + tile.0) as usize].content = Content::Air;
}
}
},
}
}
}
}
fn test_rooms_intersect(rs1: RoomShape, rs2: RoomShape) -> bool
{
use RoomShape::*;
match (rs1, rs2)
{
(Rect(r1), Rect(r2)) => rects_have_intersection_or_common_edge(r1, r2),
(Rect(r), Circ(c, rad)) |
(Circ(c, rad), Rect(r)) =>
{
r.grow(1).iter().fold(false, |b, t| b | ((c - t).l2_sq() < rad * rad))
},
(Circ(c1, r1), Circ(c2, r2)) =>
{
let dist2 = ((c2 - c1).l2_sq() as f64).sqrt();
(r1 as f64).sqrt() + (r2 as f64).sqrt() > dist2 + 1.0
},
}
}
fn hash_unique_pair<H: Hasher>(coords: V2i, rs1: &RoomShape, rs2: &RoomShape, hasher: &mut H)
{
let rs1 = rs1.into_unique_form(coords);
let rs2 = rs2.into_unique_form(coords);
rs1.hash(hasher);
rs2.hash(hasher);
}
fn get_path_endpoint<R: Rng>(rs: &RoomShape, rg: &mut R) -> V2i
{
use RoomShape::*;
match rs
{
Circ(c, _) => *c,
Rect(r) =>
{
let bd = Bernoulli::new(USE_RECTANGLE_CENTER_FOR_PATH_END_PROB).unwrap();
if bd.sample(rg)
{
r.origin() + r.dim() / V2i(2, 2)
} else
{
let V2i(xd, yd) = r.dim();
let unif = Uniform::new(0, 2 * (xd + yd) + 4);
get_rect_edge_by_index(r.grow(1), unif.sample(rg))
}
}
}
}
fn carve_between(seed: u64, coords: V2i, rs1: &RoomShape, rs2: &RoomShape) -> Option<Path>
{
if rs2 < rs1 { return carve_between(seed, coords, rs2, rs1); }
let mut hasher = DefaultHasher::new();
seed.hash(&mut hasher);
hash_unique_pair(coords, &rs1, &rs2, &mut hasher);
let mut rg = SmallRng::seed_from_u64(hasher.finish());
let mut ep1 = get_path_endpoint(&rs1, &mut rg);
let mut ep2 = get_path_endpoint(&rs2, &mut rg);
if Bernoulli::new(0.5).unwrap().sample(&mut rg) { std::mem::swap(&mut ep1, &mut ep2); }
let chooser = WeightedIndex::new(&PATH_STRATEGY_WEIGHTS).unwrap();
match chooser.sample(&mut rg)
{
0 => None,
1 =>
{
Some(Path::Line(ep1, ep2))
},
2 =>
{
let brect = R2i::origin_opp(ep1, ep2);
let V2i(xd, yd) = brect.dim();
let area = xd * yd;
if area == 0 { return None; }
let npoints: u64 = Poisson::new(PATH_ORTHOG_AVG_NUM_STOPS_PER_LEN * (area as f64).sqrt())
.unwrap().sample(&mut rg);
let mut tpoints = Vec::new();
tpoints.push(ep1);
for _ii in 0 .. npoints
{
tpoints.push(gen_rand_in_rect(&brect, &mut rg));
}
tpoints.push(ep2);
tpoints.sort_by(|a, b| (*a - ep1).linf().cmp(&(*b - ep1).linf()));
let dir = V2i((ep2.0 - ep1.0).signum(), (ep2.1 - ep1.1).signum());
let mut paths = Vec::new();
for ii in 0 .. npoints as usize + 1
{
let diff = tpoints[ii] - ep1;
let diffn = tpoints[ii + 1] - ep1;
let linf = (tpoints[ii + 1] - ep1).linf();
let stop1 = if diff.0.abs() > diff.1.abs()
{
V2i(linf * dir.0 + ep1.0, tpoints[ii].1)
} else
{
V2i(tpoints[ii].0, linf * dir.1 + ep1.1)
};
let do_stop2 = (diff.0.abs() > diff.1.abs()) ^ (diffn.0.abs() > diffn.1.abs());
if do_stop2
{
let stop2 = ep1 + V2i(linf, linf) * dir;
paths.push(Path::Line(tpoints[ii], stop1));
paths.push(Path::Line(stop1, stop2));
paths.push(Path::Line(stop2, tpoints[ii + 1]));
} else
{
paths.push(Path::Line(tpoints[ii], stop1));
paths.push(Path::Line(stop1, tpoints[ii + 1]));
}
}
Some(Path::Piecewise(paths))
}
3 =>
{
let brect = R2i::origin_opp(ep1, ep2);
let bcirc = RoomShape::Circ(brect.origin() + brect.dim() / V2i(2, 2), (brect.dim().l2_sq() as f64).sqrt().div(2.0).ceil() as isize);
let V2i(xd, yd) = brect.dim();
let area = xd * yd;
if area == 0 { return None; }
let npoints: u64 = Poisson::new(PATH_CIRC_AVG_NUM_STOPS_PER_LEN * (area as f64).sqrt())
.unwrap().sample(&mut rg);
let mut tpoints = Vec::new();
tpoints.push(ep1);
for _ii in 0 .. npoints
{
tpoints.push(bcirc.gen_rand_in(&mut rg));
}
tpoints.push(ep2);
fn s(a: V2i) -> isize { a.0 + a.1 }
fn dot(a: V2i, b: V2i) -> isize { s(a * b) }
let diff = ep2 - ep1;
tpoints.sort_by(|a, b| dot(*a - ep1, diff).cmp(&dot(*b - ep1, diff)));
let mut stops = Vec::new();
for ii in 0 .. npoints as usize + 1
{
stops.push(Path::Line(tpoints[ii], tpoints[ii + 1]));
}
Some(Path::Piecewise(stops))
}
_ => panic!("random index out of range in worldgen (path generation - unknown path strategy)"),
}
}
fn maybe_carve_between(seed: u64, coords: V2i, rs1: &RoomShape, rs2: &RoomShape, radius: isize) -> Option<Path>
{
let o1 = rs1.origin().div_euclid(V2i(GRID_SIZE, GRID_SIZE));
let o2 = rs2.origin().div_euclid(V2i(GRID_SIZE, GRID_SIZE));
if (o1 - o2).linf() <= radius
{
carve_between(seed, coords, rs1, rs2)
} else
{
None
}
}
fn carve_paths<T: Iterator<Item=Path>>(map: &mut [Tile], paths: T)
{
use Path::*;
let gbox = R2i::origin_dim(V2i(0, 0), V2i(GRID_SIZE, GRID_SIZE));
for path in paths
{
match path
{
Piecewise(v) =>
{
carve_paths(map, v.into_iter());
},
Line(p1, p2) =>
{
let lineiter = raster::line(p1, p2);
for tile in lineiter
{
if gbox.contains(tile)
{
map[(tile.1 * GRID_SIZE + tile.0) as usize].content = Content::Air;
}
}
}
}
}
}
pub fn carve(seed: u64, grid: &mut Grid<Tile>, coords: V2i)
{
let rooms = get_nearby_room_shapes(seed, coords, 1).collect::<Vec<_>>();
carve_rooms(grid.array_mut(), rooms.iter().map(|c| c.clone()));
let cpi = rooms.iter().map(|a| rooms.iter().map(move |b| (a, b))).flatten().map(|(a, b)| (a, b));
let paths = cpi.map(|(a, b)| maybe_carve_between(seed, coords, a, b, 1)).filter_map(|o| o);
carve_paths(grid.array_mut(), paths);
}
#[cfg(test)]
mod test
{
use super::*;
const GRID_USIZE: usize = GRID_SIZE as usize;
#[test]
fn list_rooms()
{
for ii in 0u64..1u64
{
let rooms = get_nearby_room_shapes(ii, V2i(0, 0), 1);
println!("rooms: {:?}", rooms.collect::<Vec<_>>());
}
}
#[test]
fn show_rooms()
{
for seed in 0u64..20u64
{
let origin = V2i(0, 0);
let mut map = vec![Tile { content: Content::Stone }; GRID_USIZE * GRID_USIZE];
carve_rooms(&mut map[..], get_nearby_room_shapes(seed, origin, 1));
for x in 0 .. GRID_USIZE
{
for y in 0 .. GRID_USIZE
{
print!("{}", if let Tile{ content: Content::Air } = map[x * GRID_USIZE + y] { ' ' } else { '#' });
}
println!("");
}
println!("");
}
}
#[test]
fn rect_edge_test()
{
let (xd, yd) = (3 ,5);
let r = R2i::origin_dim(V2i(0, 0), V2i(xd, yd));
println!("rectangle: {:?}", r);
for i in 0 .. 2 * (xd + yd) - 4
{
let e = get_rect_edge_by_index(r, i);
println!("edge at: {:?}", e);
assert!(r.contains(e));
}
}
#[test]
fn single_path_test()
{
for seed in 0u64..20u64
{
let origin = V2i(0, 0);
let mut map = vec![Tile { content: Content::Stone }; GRID_USIZE * GRID_USIZE];
let shape1 = RoomShape::Circ(V2i(0, 0), 0);
let shape2 = RoomShape::Circ(V2i(GRID_SIZE - 1, GRID_SIZE - 1), 0);
carve_paths(&mut map[..], carve_between(seed, origin, &shape1, &shape2).into_iter());
for x in 0 .. GRID_USIZE
{
for y in 0 .. GRID_USIZE
{
print!("{}", if let Tile{ content: Content::Air } = map[x * GRID_USIZE + y] { ' ' } else { '#' });
}
println!("");
}
println!("");
}
}
}