use serde::{Deserialize, Serialize};
use crate::world_segment::ids::ClusterId;
use crate::world_segment::stitch::Build;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Tier {
Confident,
Probable,
Debris,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Signal {
pub name: String,
pub value: f64,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct ScoreConfig {
pub debris_max_blocks: u64,
pub confident_min_blocks: u64,
pub confident_min_density: f64,
}
impl Default for ScoreConfig {
fn default() -> Self {
ScoreConfig {
debris_max_blocks: 100,
confident_min_blocks: 1000,
confident_min_density: 0.02,
}
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Scored {
pub build_id: ClusterId,
pub tier: Tier,
pub signals: Vec<Signal>,
}
fn volume(bbox: ((i32, i32, i32), (i32, i32, i32))) -> u64 {
let dx = (bbox.1 .0 - bbox.0 .0 + 1).max(1) as u64;
let dy = (bbox.1 .1 - bbox.0 .1 + 1).max(1) as u64;
let dz = (bbox.1 .2 - bbox.0 .2 + 1).max(1) as u64;
dx * dy * dz
}
pub fn score(build: &Build, cfg: &ScoreConfig) -> Scored {
let vol = volume(build.bbox);
let density = build.block_count as f64 / vol as f64;
let signals = vec![
Signal {
name: "block_count".into(),
value: build.block_count as f64,
},
Signal {
name: "cell_count".into(),
value: build.cell_count as f64,
},
Signal {
name: "bbox_volume".into(),
value: vol as f64,
},
Signal {
name: "density".into(),
value: density,
},
Signal {
name: "cluster_count".into(),
value: build.cluster_ids.len() as f64,
},
];
let tier = if build.block_count <= cfg.debris_max_blocks {
Tier::Debris
} else if build.block_count >= cfg.confident_min_blocks && density >= cfg.confident_min_density
{
Tier::Confident
} else {
Tier::Probable
};
Scored {
build_id: build.id,
tier,
signals,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::world_segment::ids::{ClusterId, ContentId, TileId};
use crate::world_segment::stitch::Build;
fn build(blocks: u64, bbox: ((i32, i32, i32), (i32, i32, i32))) -> Build {
let id = ClusterId::new(
ContentId::of(&[b"x"]),
TileId { x: 0, z: 0 },
None,
(0, 0, 0),
);
Build {
id,
cluster_ids: vec![id],
bbox,
block_count: blocks,
cell_count: blocks / 4 + 1,
partition_id: None,
}
}
fn cfg() -> ScoreConfig {
ScoreConfig {
debris_max_blocks: 20,
confident_min_blocks: 500,
confident_min_density: 0.05,
}
}
#[test]
fn tiny_clusters_are_debris() {
let s = score(&build(5, ((0, 0, 0), (1, 1, 1))), &cfg());
assert_eq!(s.tier, Tier::Debris);
}
#[test]
fn large_dense_builds_are_confident() {
let s = score(&build(1000, ((0, 0, 0), (9, 9, 9))), &cfg());
assert_eq!(s.tier, Tier::Confident);
}
#[test]
fn large_but_sparse_builds_are_probable() {
let s = score(&build(100, ((0, 0, 0), (49, 49, 49))), &cfg());
assert_eq!(s.tier, Tier::Probable);
}
#[test]
fn signals_are_recorded_and_named() {
let s = score(&build(1000, ((0, 0, 0), (9, 9, 9))), &cfg());
assert!(s
.signals
.iter()
.any(|sg| sg.name == "block_count" && sg.value == 1000.0));
assert!(s
.signals
.iter()
.any(|sg| sg.name == "density" && sg.value == 1.0));
}
#[test]
fn score_config_round_trips_through_serde_json() {
let c = cfg();
let round_tripped: ScoreConfig =
serde_json::from_str(&serde_json::to_string(&c).unwrap()).unwrap();
assert_eq!(c, round_tripped);
}
}