From e30c625d81559d3d5c8982c20c022720b7af3a2a Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 6 Feb 2021 23:53:25 +0000 Subject: [PATCH] More tree variety, denser forests --- common/src/store.rs | 42 ++++--- world/examples/site.rs | 11 +- world/src/layer/tree.rs | 236 ++++++++++++++++++++++++---------------- world/src/sim/mod.rs | 2 +- world/src/site2/mod.rs | 26 ++--- world/src/site2/plot.rs | 6 +- world/src/site2/tile.rs | 22 ++-- 7 files changed, 199 insertions(+), 146 deletions(-) diff --git a/common/src/store.rs b/common/src/store.rs index 49ed00e8c4..3ba7b9ab0d 100644 --- a/common/src/store.rs +++ b/common/src/store.rs @@ -31,7 +31,13 @@ impl PartialEq for Id { } impl fmt::Debug for Id { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Id<{}>({}, {})", std::any::type_name::(), self.idx, self.gen) + write!( + f, + "Id<{}>({}, {})", + std::any::type_name::(), + self.idx, + self.gen + ) } } impl hash::Hash for Id { @@ -69,9 +75,7 @@ impl Store { } pub fn get(&self, id: Id) -> &T { - let entry = self.entries - .get(id.idx as usize) - .unwrap(); + let entry = self.entries.get(id.idx as usize).unwrap(); if entry.gen == id.gen { entry.item.as_ref().unwrap() } else { @@ -80,9 +84,7 @@ impl Store { } pub fn get_mut(&mut self, id: Id) -> &mut T { - let entry = self.entries - .get_mut(id.idx as usize) - .unwrap(); + let entry = self.entries.get_mut(id.idx as usize).unwrap(); if entry.gen == id.gen { entry.item.as_mut().unwrap() } else { @@ -90,13 +92,9 @@ impl Store { } } - pub fn ids(&self) -> impl Iterator> + '_ { - self.iter().map(|(id, _)| id) - } + pub fn ids(&self) -> impl Iterator> + '_ { self.iter().map(|(id, _)| id) } - pub fn values(&self) -> impl Iterator + '_ { - self.iter().map(|(_, item)| item) - } + pub fn values(&self) -> impl Iterator + '_ { self.iter().map(|(_, item)| item) } pub fn values_mut(&mut self) -> impl Iterator + '_ { self.iter_mut().map(|(_, item)| item) @@ -111,7 +109,8 @@ impl Store { idx: idx as u32, gen: entry.gen, phantom: PhantomData, - }).zip(entry.item.as_ref()) + }) + .zip(entry.item.as_ref()) }) } @@ -124,14 +123,16 @@ impl Store { idx: idx as u32, gen: entry.gen, phantom: PhantomData, - }).zip(entry.item.as_mut()) + }) + .zip(entry.item.as_mut()) }) } pub fn insert(&mut self, item: T) -> Id { if self.len < self.entries.len() { // TODO: Make this more efficient with a lookahead system - let (idx, entry) = self.entries + let (idx, entry) = self + .entries .iter_mut() .enumerate() .find(|(_, e)| e.item.is_none()) @@ -161,13 +162,10 @@ impl Store { } pub fn remove(&mut self, id: Id) -> Option { - if let Some(item) = self.entries + if let Some(item) = self + .entries .get_mut(id.idx as usize) - .and_then(|e| if e.gen == id.gen { - e.item.take() - } else { - None - }) + .and_then(|e| if e.gen == id.gen { e.item.take() } else { None }) { self.len -= 1; Some(item) diff --git a/world/examples/site.rs b/world/examples/site.rs index 05c01a34c9..b55e63026e 100644 --- a/world/examples/site.rs +++ b/world/examples/site.rs @@ -5,7 +5,10 @@ use veloren_world::site2::test_site; fn main() { let site = test_site(); let size = site.bounds().size(); - println!("{}", BeginSvg { w: size.w as f32, h: size.h as f32 }); + println!("{}", BeginSvg { + w: size.w as f32, + h: size.h as f32 + }); for plot in site.plots() { let bounds = plot.find_bounds(); @@ -15,7 +18,11 @@ fn main() { w: bounds.size().w as f32, h: bounds.size().h as f32, style: Style { - fill: Fill::Color(Color { r: 50, g: 50, b: 50 }), + fill: Fill::Color(Color { + r: 50, + g: 50, + b: 50 + }), stroke: Stroke::Color(Color { r: 0, g: 0, b: 0 }, 1.0), opacity: 1.0, stroke_opacity: 1.0, diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs index d130cbf8a4..ba2d2fb5e7 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -7,14 +7,17 @@ use crate::{ }; use common::{ assets::AssetHandle, - terrain::{Block, BlockKind, structure::{Structure, StructureBlock, StructuresGroup}}, + terrain::{ + structure::{Structure, StructureBlock, StructuresGroup}, + Block, BlockKind, + }, vol::ReadVol, }; use hashbrown::HashMap; use lazy_static::lazy_static; +use rand::prelude::*; use std::{f32, ops::Range}; use vek::*; -use rand::prelude::*; lazy_static! { static ref OAKS: AssetHandle = Structure::load_group("oaks"); @@ -99,15 +102,25 @@ pub fn apply_trees_to(canvas: &mut Canvas) { ForestKind::Acacia => *ACACIAS, ForestKind::Baobab => *BAOBABS, // ForestKind::Oak => *OAKS, - ForestKind::Oak => break 'model TreeModel::Procedural( - ProceduralTree::generate(TreeConfig::OAK, seed), - StructureBlock::TemperateLeaves, - ), + ForestKind::Oak => { + break 'model TreeModel::Procedural( + ProceduralTree::generate( + TreeConfig::oak(&mut RandomPerm::new(seed)), + &mut RandomPerm::new(seed), + ), + StructureBlock::TemperateLeaves, + ); + }, //ForestKind::Pine => *PINES, - ForestKind::Pine => break 'model TreeModel::Procedural( - ProceduralTree::generate(TreeConfig::PINE, seed), - StructureBlock::PineLeaves, - ), + ForestKind::Pine => { + break 'model TreeModel::Procedural( + ProceduralTree::generate( + TreeConfig::pine(&mut RandomPerm::new(seed)), + &mut RandomPerm::new(seed), + ), + StructureBlock::PineLeaves, + ); + }, ForestKind::Birch => *BIRCHES, ForestKind::Mangrove => *MANGROVE_TREES, ForestKind::Swamp => *SWAMP_TREES, @@ -115,8 +128,11 @@ pub fn apply_trees_to(canvas: &mut Canvas) { }; let models = models.read(); - TreeModel::Structure(models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize % models.len()] - .clone()) + TreeModel::Structure( + models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize + % models.len()] + .clone(), + ) }, seed, units: UNIT_CHOOSER.get(seed), @@ -133,9 +149,7 @@ pub fn apply_trees_to(canvas: &mut Canvas) { }; let rpos2d = (wpos2d - tree.pos.xy()) - .map2(Vec2::new(tree.units.0, tree.units.1), |p, unit| { - unit * p - }) + .map2(Vec2::new(tree.units.0, tree.units.1), |p, unit| unit * p) .sum(); if !Aabr::from(bounds).contains_point(rpos2d) { // Skip this column @@ -158,15 +172,17 @@ pub fn apply_trees_to(canvas: &mut Canvas) { info.index(), if let Some(block) = match &tree.model { TreeModel::Structure(s) => s.get(model_pos).ok().copied(), - TreeModel::Procedural(t, leaf_block) => Some(match t.is_branch_or_leaves_at(model_pos.map(|e| e as f32 + 0.5)) { - (true, _) => StructureBlock::Normal(Rgb::new(60, 30, 0)), - (_, true) => *leaf_block, - (_, _) => StructureBlock::None, - }), + TreeModel::Procedural(t, leaf_block) => Some( + match t.is_branch_or_leaves_at(model_pos.map(|e| e as f32 + 0.5)) { + (true, _) => StructureBlock::Normal(Rgb::new(60, 30, 0)), + (_, true) => *leaf_block, + (_, _) => StructureBlock::None, + }, + ), } { block } else { - break + break; }, wpos, tree.pos.xy(), @@ -215,50 +231,62 @@ pub struct TreeConfig { pub max_depth: usize, /// The number of branches that form from each branch. pub splits: usize, - /// The range of proportions along a branch at which a split into another branch might occur. - /// This value is clamped between 0 and 1, but a wider range may bias the results towards branch ends. + /// The range of proportions along a branch at which a split into another + /// branch might occur. This value is clamped between 0 and 1, but a + /// wider range may bias the results towards branch ends. pub split_range: Range, - /// The bias applied to the length of branches based on the proportion along their parent that they eminate from. - /// -1.0 = negative bias (branches at ends are longer, branches at the start are shorter) - /// 0.0 = no bias (branches do not change their length with regard to parent branch proportion) - /// 1.0 = positive bias (branches at ends are shorter, branches at the start are longer) + /// The bias applied to the length of branches based on the proportion along + /// their parent that they eminate from. -1.0 = negative bias (branches + /// at ends are longer, branches at the start are shorter) 0.0 = no bias + /// (branches do not change their length with regard to parent branch + /// proportion) 1.0 = positive bias (branches at ends are shorter, + /// branches at the start are longer) pub branch_len_bias: f32, - /// The scale of leaves in the vertical plane. Less than 1.0 implies a flattening of the leaves. + /// The scale of leaves in the vertical plane. Less than 1.0 implies a + /// flattening of the leaves. pub leaf_vertical_scale: f32, /// How evenly spaced (vs random) sub-branches are along their parent. pub proportionality: f32, } impl TreeConfig { - pub const OAK: Self = Self { - trunk_len: 12.0, - trunk_radius: 3.0, - branch_child_len: 0.8, - branch_child_radius: 0.6, - leaf_radius: 3.0..5.0, - straightness: 0.5, - max_depth: 4, - splits: 3, - split_range: 0.5..1.5, - branch_len_bias: 0.0, - leaf_vertical_scale: 1.0, - proportionality: 0.0, - }; + pub fn oak(rng: &mut impl Rng) -> Self { + let scale = Lerp::lerp(0.8, 1.5, rng.gen::().powi(4)); - pub const PINE: Self = Self { - trunk_len: 32.0, - trunk_radius: 1.5, - branch_child_len: 0.3, - branch_child_radius: 0.0, - leaf_radius: 2.0..2.5, - straightness: 0.0, - max_depth: 1, - splits: 56, - split_range: 0.2..1.2, - branch_len_bias: 0.75, - leaf_vertical_scale: 0.3, - proportionality: 1.0, - }; + Self { + trunk_len: 12.0 * scale, + trunk_radius: 3.0 * scale, + branch_child_len: 0.8, + branch_child_radius: 0.6, + leaf_radius: 3.0 * scale..4.0 * scale, + straightness: 0.5, + max_depth: 4, + splits: 3, + split_range: 0.5..1.5, + branch_len_bias: 0.0, + leaf_vertical_scale: 1.0, + proportionality: 0.0, + } + } + + pub fn pine(rng: &mut impl Rng) -> Self { + let scale = Lerp::lerp(1.0, 2.0, rng.gen::().powi(4)); + + Self { + trunk_len: 32.0 * scale, + trunk_radius: 1.5 * scale, + branch_child_len: 0.3, + branch_child_radius: 0.0, + leaf_radius: 2.0 * scale..2.5 * scale, + straightness: 0.0, + max_depth: 1, + splits: 56, + split_range: 0.2..1.2, + branch_len_bias: 0.75, + leaf_vertical_scale: 0.3, + proportionality: 1.0, + } + } } // TODO: Rename this to `Tree` when the name conflict is gone @@ -269,9 +297,7 @@ pub struct ProceduralTree { impl ProceduralTree { /// Generate a new tree using the given configuration and seed. - pub fn generate(config: TreeConfig, seed: u32) -> Self { - let mut rng = RandomPerm::new(seed); - + pub fn generate(config: TreeConfig, rng: &mut impl Rng) -> Self { let mut this = Self { branches: Vec::new(), trunk_idx: 0, // Gets replaced later @@ -283,20 +309,21 @@ impl ProceduralTree { // Our trunk starts at the origin... Vec3::zero(), // ...and has a roughly upward direction - Vec3::new(rng.gen_range(-1.0..1.0), rng.gen_range(-1.0..1.0), 5.0).normalized(), + Vec3::new(rng.gen_range(-1.0..1.0), rng.gen_range(-1.0..1.0), 10.0).normalized(), config.trunk_len, config.trunk_radius, 0, None, - &mut rng, + rng, ); this.trunk_idx = trunk_idx; this } - // Recursively add a branch (with sub-branches) to the tree's branch graph, returning the index and AABB of the - // branch. This AABB gets propagated down to the parent and is used later during sampling to cull the branches to + // Recursively add a branch (with sub-branches) to the tree's branch graph, + // returning the index and AABB of the branch. This AABB gets propagated + // down to the parent and is used later during sampling to cull the branches to // be sampled. fn add_branch( &mut self, @@ -318,7 +345,8 @@ impl ProceduralTree { 0.0 }; - // The AABB that covers this branch, along with wood and leaves that eminate from it + // The AABB that covers this branch, along with wood and leaves that eminate + // from it let mut aabb = Aabb { min: Vec3::partial_min(start, end) - wood_radius.max(leaf_radius), max: Vec3::partial_max(start, end) + wood_radius.max(leaf_radius), @@ -327,31 +355,43 @@ impl ProceduralTree { let mut child_idx = None; // Don't add child branches if we're already enough layers into the tree if depth < config.max_depth { - let x_axis = dir.cross(Vec3::::zero().map(|_| rng.gen_range(-1.0..1.0))).normalized(); + let x_axis = dir + .cross(Vec3::::zero().map(|_| rng.gen_range(-1.0..1.0))) + .normalized(); let y_axis = dir.cross(x_axis).normalized(); let screw_shift = rng.gen_range(0.0..f32::consts::TAU); for i in 0..config.splits { - let dist = Lerp::lerp(i as f32 / (config.splits - 1) as f32, rng.gen_range(0.0..1.0), config.proportionality); + let dist = Lerp::lerp( + i as f32 / (config.splits - 1) as f32, + rng.gen_range(0.0..1.0), + config.proportionality, + ); const PHI: f32 = 0.618; const RAD_PER_BRANCH: f32 = f32::consts::TAU * PHI; - let screw = - (screw_shift + dist * config.splits as f32 * RAD_PER_BRANCH).sin() * x_axis + - (screw_shift + dist * config.splits as f32 * RAD_PER_BRANCH).cos() * y_axis; + let screw = (screw_shift + dist * config.splits as f32 * RAD_PER_BRANCH).sin() + * x_axis + + (screw_shift + dist * config.splits as f32 * RAD_PER_BRANCH).cos() * y_axis; - // Choose a point close to the branch to act as the target direction for the branch to grow in - // let split_factor = rng.gen_range(config.split_range.start, config.split_range.end).clamped(0.0, 1.0); - let split_factor = Lerp::lerp(config.split_range.start, config.split_range.end, dist); - let tgt = Lerp::lerp_unclamped( - start, - end, - split_factor, - ) + Lerp::lerp(Vec3::::zero().map(|_| rng.gen_range(-1.0..1.0)), screw, config.proportionality); + // Choose a point close to the branch to act as the target direction for the + // branch to grow in let split_factor = + // rng.gen_range(config.split_range.start, config.split_range.end).clamped(0.0, + // 1.0); + let split_factor = + Lerp::lerp(config.split_range.start, config.split_range.end, dist); + let tgt = Lerp::lerp_unclamped(start, end, split_factor) + + Lerp::lerp( + Vec3::::zero().map(|_| rng.gen_range(-1.0..1.0)), + screw, + config.proportionality, + ); // Start the branch at the closest point to the target let branch_start = line.projected_point(tgt); - // Now, interpolate between the target direction and the parent branch's direction to find a direction - let branch_dir = Lerp::lerp(tgt - branch_start, dir, config.straightness).normalized(); + // Now, interpolate between the target direction and the parent branch's + // direction to find a direction + let branch_dir = + Lerp::lerp(tgt - branch_start, dir, config.straightness).normalized(); let (branch_idx, branch_aabb) = self.add_branch( config, @@ -359,14 +399,18 @@ impl ProceduralTree { branch_dir, branch_len * config.branch_child_len - * (1.0 - (split_factor - 0.5) * 2.0 * config.branch_len_bias.clamped(-1.0, 1.0)), + * (1.0 + - (split_factor - 0.5) + * 2.0 + * config.branch_len_bias.clamped(-1.0, 1.0)), branch_radius * config.branch_child_radius, depth + 1, child_idx, rng, ); child_idx = Some(branch_idx); - // Parent branches AABBs include the AABBs of child branches to allow for culling during sampling + // Parent branches AABBs include the AABBs of child branches to allow for + // culling during sampling aabb.expand_to_contain(branch_aabb); } } @@ -386,19 +430,20 @@ impl ProceduralTree { } /// Get the bounding box that covers the tree (all branches and leaves) - pub fn get_bounds(&self) -> Aabb { - self.branches[self.trunk_idx].aabb - } + pub fn get_bounds(&self) -> Aabb { self.branches[self.trunk_idx].aabb } // Recursively search for branches or leaves by walking the tree's branch graph. fn is_branch_or_leaves_at_inner(&self, pos: Vec3, branch_idx: usize) -> (bool, bool) { let branch = &self.branches[branch_idx]; - // Always probe the sibling branch, since our AABB doesn't include its bounds (it's not one of our children) - let branch_or_leaves = branch.sibling_idx + // Always probe the sibling branch, since our AABB doesn't include its bounds + // (it's not one of our children) + let branch_or_leaves = branch + .sibling_idx .map(|idx| Vec2::from(self.is_branch_or_leaves_at_inner(pos, idx))) .unwrap_or_default(); - // Only continue probing this sub-graph of the tree if the sample position falls within its AABB + // Only continue probing this sub-graph of the tree if the sample position falls + // within its AABB if branch.aabb.contains_point(pos) { (branch_or_leaves // Probe this branch @@ -407,23 +452,25 @@ impl ProceduralTree { | branch.child_idx .map(|idx| Vec2::from(self.is_branch_or_leaves_at_inner(pos, idx))) .unwrap_or_default()) - .into_tuple() + .into_tuple() } else { branch_or_leaves.into_tuple() } } - /// Determine whether there are either branches or leaves at the given position in the tree. + /// Determine whether there are either branches or leaves at the given + /// position in the tree. #[inline(always)] pub fn is_branch_or_leaves_at(&self, pos: Vec3) -> (bool, bool) { self.is_branch_or_leaves_at_inner(pos, self.trunk_idx) } } -// Branches are arranged in a graph shape. Each branch points to both its first child (if any) and also to the next -// branch in the list of child branches associated with the parent. This means that the entire tree is laid out in a -// walkable graph where each branch refers only to two other branches. As a result, walking the tree is simply a case -// of performing double recursion. +// Branches are arranged in a graph shape. Each branch points to both its first +// child (if any) and also to the next branch in the list of child branches +// associated with the parent. This means that the entire tree is laid out in a +// walkable graph where each branch refers only to two other branches. As a +// result, walking the tree is simply a case of performing double recursion. struct Branch { line: LineSegment3, wood_radius: f32, @@ -436,7 +483,8 @@ struct Branch { } impl Branch { - /// Determine whether there are either branches or leaves at the given position in the branch. + /// Determine whether there are either branches or leaves at the given + /// position in the branch. pub fn is_branch_or_leaves_at(&self, pos: Vec3) -> (bool, bool) { // fn finvsqrt(x: f32) -> f32 { // let y = f32::from_bits(0x5f375a86 - (x.to_bits() >> 1)); diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 7d9d4c1590..4ec3e89b3e 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -516,7 +516,7 @@ impl WorldSim { cave_0_nz: SuperSimplex::new().set_seed(rng.gen()), cave_1_nz: SuperSimplex::new().set_seed(rng.gen()), - structure_gen: StructureGen2d::new(rng.gen(), 32, 16), + structure_gen: StructureGen2d::new(rng.gen(), 24, 8), region_gen: StructureGen2d::new(rng.gen(), 400, 96), humid_nz: Billow::new() .set_octaves(9) diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index e580fdfc9f..916e4fec9a 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -1,14 +1,14 @@ -mod tile; mod plot; +mod tile; -use vek::*; -use common::store::{Store, Id}; -use crate::util::Grid; use self::{ - tile::TileGrid, plot::{Plot, PlotKind}, + tile::TileGrid, }; +use crate::util::Grid; +use common::store::{Id, Store}; use rand::prelude::*; +use vek::*; #[derive(Default)] pub struct Site { @@ -25,19 +25,17 @@ impl Site { } } - pub fn plots(&self) -> impl Iterator + '_ { - self.plots.values() - } + pub fn plots(&self) -> impl Iterator + '_ { self.plots.values() } - pub fn create_plot(&mut self, plot: Plot) -> Id { - self.plots.insert(plot) - } + pub fn create_plot(&mut self, plot: Plot) -> Id { self.plots.insert(plot) } pub fn generate(rng: &mut impl Rng) -> Self { let mut site = Site::default(); for i in 0..10 { - let dir = Vec2::::zero().map(|_| rng.gen_range(-1.0..1.0)).normalized(); + let dir = Vec2::::zero() + .map(|_| rng.gen_range(-1.0..1.0)) + .normalized(); let search_pos = (dir * 32.0).map(|e| e as i32); site.tiles @@ -51,6 +49,4 @@ impl Site { } } -pub fn test_site() -> Site { - Site::generate(&mut thread_rng()) -} +pub fn test_site() -> Site { Site::generate(&mut thread_rng()) } diff --git a/world/src/site2/plot.rs b/world/src/site2/plot.rs index 5b8627c25d..02175c4ac0 100644 --- a/world/src/site2/plot.rs +++ b/world/src/site2/plot.rs @@ -1,5 +1,5 @@ -use vek::*; use crate::util::DHashSet; +use vek::*; pub struct Plot { kind: PlotKind, @@ -11,7 +11,9 @@ impl Plot { pub fn find_bounds(&self) -> Aabr { self.tiles .iter() - .fold(Aabr::new_empty(self.root_tile), |b, t| b.expanded_to_contain_point(*t)) + .fold(Aabr::new_empty(self.root_tile), |b, t| { + b.expanded_to_contain_point(*t) + }) } } diff --git a/world/src/site2/tile.rs b/world/src/site2/tile.rs index 4d7fa35f00..2fc225d19c 100644 --- a/world/src/site2/tile.rs +++ b/world/src/site2/tile.rs @@ -23,18 +23,22 @@ impl TileGrid { let tpos = tpos + TILE_RADIUS as i32; self.zones .get(tpos) - .and_then(|zone| zone.as_ref()?.get(tpos.map(|e| e.rem_euclid(ZONE_SIZE as i32)))) + .and_then(|zone| { + zone.as_ref()? + .get(tpos.map(|e| e.rem_euclid(ZONE_SIZE as i32))) + }) .and_then(|tile| tile.as_ref()) } pub fn get_mut(&mut self, tpos: Vec2) -> Option<&mut Tile> { let tpos = tpos + TILE_RADIUS as i32; - self.zones - .get_mut(tpos) - .and_then(|zone| zone - .get_or_insert_with(|| Grid::populate_from(Vec2::broadcast(ZONE_RADIUS as i32 * 2 + 1), |_| None)) - .get_mut(tpos.map(|e| e.rem_euclid(ZONE_SIZE as i32))) - .map(|tile| tile.get_or_insert_with(|| Tile::empty()))) + self.zones.get_mut(tpos).and_then(|zone| { + zone.get_or_insert_with(|| { + Grid::populate_from(Vec2::broadcast(ZONE_RADIUS as i32 * 2 + 1), |_| None) + }) + .get_mut(tpos.map(|e| e.rem_euclid(ZONE_SIZE as i32))) + .map(|tile| tile.get_or_insert_with(|| Tile::empty())) + }) } pub fn find_near(&self, tpos: Vec2, f: impl Fn(&Tile) -> bool) -> Option> { @@ -62,7 +66,5 @@ impl Tile { } } - pub fn is_empty(&self) -> bool { - self.kind == TileKind::Empty - } + pub fn is_empty(&self) -> bool { self.kind == TileKind::Empty } }