M1a: deterministischer Rust-Kern #3
@@ -8,4 +8,5 @@
|
|||||||
|
|
||||||
pub mod codec;
|
pub mod codec;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
|
pub mod map;
|
||||||
pub mod rng;
|
pub mod rng;
|
||||||
|
|||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
//! Tile map of the shared world.
|
||||||
|
|
||||||
|
use crate::codec::{DecodeError, Reader, Writer};
|
||||||
|
|
||||||
|
/// Longest accepted map edge, in tiles.
|
||||||
|
pub const MAX_MAP_SIZE: u32 = 1024;
|
||||||
|
|
||||||
|
/// Tile coordinate. `x` grows to the right, `y` grows downwards.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub struct Pos {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pos {
|
||||||
|
pub const fn new(x: i32, y: i32) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a tile is made of.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum Terrain {
|
||||||
|
/// Open ground that can be walked on.
|
||||||
|
Ground = 0,
|
||||||
|
/// Rock that a stone pickaxe can break.
|
||||||
|
SoftRock = 1,
|
||||||
|
/// Rock that needs a copper pickaxe.
|
||||||
|
HardRock = 2,
|
||||||
|
/// Indestructible rock at the edge of the map.
|
||||||
|
Bedrock = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terrain {
|
||||||
|
pub fn from_u8(value: u8) -> Option<Self> {
|
||||||
|
match value {
|
||||||
|
0 => Some(Terrain::Ground),
|
||||||
|
1 => Some(Terrain::SoftRock),
|
||||||
|
2 => Some(Terrain::HardRock),
|
||||||
|
3 => Some(Terrain::Bedrock),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_walkable(self) -> bool {
|
||||||
|
matches!(self, Terrain::Ground)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Number of hits that break this terrain, or `None` if it cannot be dug.
|
||||||
|
pub fn hits_to_break(self) -> Option<u8> {
|
||||||
|
match self {
|
||||||
|
Terrain::SoftRock => Some(3),
|
||||||
|
Terrain::HardRock => Some(6),
|
||||||
|
Terrain::Ground | Terrain::Bedrock => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lowest pickaxe tier that can dig this terrain, or `None` if it cannot
|
||||||
|
/// be dug.
|
||||||
|
pub fn required_tool_tier(self) -> Option<u8> {
|
||||||
|
match self {
|
||||||
|
Terrain::SoftRock => Some(1),
|
||||||
|
Terrain::HardRock => Some(2),
|
||||||
|
Terrain::Ground | Terrain::Bedrock => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One cell of the map.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Tile {
|
||||||
|
pub terrain: Terrain,
|
||||||
|
/// Hits this tile has taken so far. Always below the number of hits that
|
||||||
|
/// break its terrain, and zero for terrain that cannot be dug.
|
||||||
|
pub damage: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tile {
|
||||||
|
pub const fn new(terrain: Terrain) -> Self {
|
||||||
|
Self { terrain, damage: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rectangular grid of tiles.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Map {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
tiles: Vec<Tile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Map {
|
||||||
|
/// Creates a map in which every tile has the given terrain.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if an edge is zero or longer than [`MAX_MAP_SIZE`].
|
||||||
|
pub fn filled(width: u32, height: u32, terrain: Terrain) -> Self {
|
||||||
|
assert!(width > 0 && height > 0, "map must not be empty");
|
||||||
|
assert!(
|
||||||
|
width <= MAX_MAP_SIZE && height <= MAX_MAP_SIZE,
|
||||||
|
"map is too large"
|
||||||
|
);
|
||||||
|
let count = (width as usize) * (height as usize);
|
||||||
|
Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
tiles: vec![Tile::new(terrain); count],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(&self, pos: Pos) -> Option<usize> {
|
||||||
|
if pos.x < 0 || pos.y < 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (x, y) = (pos.x as u32, pos.y as u32);
|
||||||
|
if x >= self.width || y >= self.height {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((y as usize) * (self.width as usize) + (x as usize))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn in_bounds(&self, pos: Pos) -> bool {
|
||||||
|
self.index(pos).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, pos: Pos) -> Option<Tile> {
|
||||||
|
self.index(pos).map(|index| self.tiles[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the tile at `pos`. Returns `false` if `pos` is outside the
|
||||||
|
/// map.
|
||||||
|
pub fn set(&mut self, pos: Pos, tile: Tile) -> bool {
|
||||||
|
match self.index(pos) {
|
||||||
|
Some(index) => {
|
||||||
|
self.tiles[index] = tile;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All tiles, row by row: the tile at `pos` has index `y * width + x`.
|
||||||
|
pub fn tiles(&self) -> &[Tile] {
|
||||||
|
&self.tiles
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode(&self, w: &mut Writer) {
|
||||||
|
w.u32(self.width);
|
||||||
|
w.u32(self.height);
|
||||||
|
for tile in &self.tiles {
|
||||||
|
w.u8(tile.terrain as u8);
|
||||||
|
w.u8(tile.damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
|
||||||
|
let width = r.u32()?;
|
||||||
|
let height = r.u32()?;
|
||||||
|
if width == 0 || height == 0 || width > MAX_MAP_SIZE || height > MAX_MAP_SIZE {
|
||||||
|
return Err(DecodeError::InvalidValue("map size"));
|
||||||
|
}
|
||||||
|
let count = (width as usize) * (height as usize);
|
||||||
|
if r.remaining() < count * 2 {
|
||||||
|
return Err(DecodeError::UnexpectedEnd);
|
||||||
|
}
|
||||||
|
let mut tiles = Vec::with_capacity(count);
|
||||||
|
for _ in 0..count {
|
||||||
|
let terrain = Terrain::from_u8(r.u8()?).ok_or(DecodeError::InvalidValue("terrain"))?;
|
||||||
|
let damage = r.u8()?;
|
||||||
|
if damage > 0 && damage >= terrain.hits_to_break().unwrap_or(0) {
|
||||||
|
return Err(DecodeError::InvalidValue("tile damage"));
|
||||||
|
}
|
||||||
|
tiles.push(Tile { terrain, damage });
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
tiles,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
use grovewright_core::codec::{DecodeError, Reader, Writer};
|
||||||
|
use grovewright_core::map::{Map, Pos, Terrain, Tile};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_filled_map_has_the_requested_size_and_terrain() {
|
||||||
|
let map = Map::filled(4, 3, Terrain::SoftRock);
|
||||||
|
assert_eq!(map.width(), 4);
|
||||||
|
assert_eq!(map.height(), 3);
|
||||||
|
assert_eq!(map.tiles().len(), 12);
|
||||||
|
assert_eq!(map.get(Pos::new(3, 2)), Some(Tile::new(Terrain::SoftRock)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positions_outside_the_map_are_rejected() {
|
||||||
|
let mut map = Map::filled(4, 3, Terrain::Ground);
|
||||||
|
for pos in [
|
||||||
|
Pos::new(-1, 0),
|
||||||
|
Pos::new(0, -1),
|
||||||
|
Pos::new(4, 0),
|
||||||
|
Pos::new(0, 3),
|
||||||
|
] {
|
||||||
|
assert!(!map.in_bounds(pos), "{pos:?}");
|
||||||
|
assert_eq!(map.get(pos), None, "{pos:?}");
|
||||||
|
assert!(!map.set(pos, Tile::new(Terrain::Bedrock)), "{pos:?}");
|
||||||
|
}
|
||||||
|
assert!(map.in_bounds(Pos::new(3, 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_changes_exactly_one_tile() {
|
||||||
|
let mut map = Map::filled(4, 3, Terrain::Ground);
|
||||||
|
assert!(map.set(Pos::new(1, 2), Tile::new(Terrain::HardRock)));
|
||||||
|
let hard_rock = map
|
||||||
|
.tiles()
|
||||||
|
.iter()
|
||||||
|
.filter(|tile| tile.terrain == Terrain::HardRock)
|
||||||
|
.count();
|
||||||
|
assert_eq!(hard_rock, 1);
|
||||||
|
assert_eq!(map.tiles()[2 * 4 + 1].terrain, Terrain::HardRock);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_rock_can_be_dug() {
|
||||||
|
assert_eq!(Terrain::Ground.hits_to_break(), None);
|
||||||
|
assert_eq!(Terrain::Bedrock.hits_to_break(), None);
|
||||||
|
assert_eq!(Terrain::SoftRock.hits_to_break(), Some(3));
|
||||||
|
assert_eq!(Terrain::HardRock.hits_to_break(), Some(6));
|
||||||
|
assert_eq!(Terrain::Ground.required_tool_tier(), None);
|
||||||
|
assert_eq!(Terrain::Bedrock.required_tool_tier(), None);
|
||||||
|
assert_eq!(Terrain::SoftRock.required_tool_tier(), Some(1));
|
||||||
|
assert_eq!(Terrain::HardRock.required_tool_tier(), Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_ground_can_be_walked_on() {
|
||||||
|
assert!(Terrain::Ground.is_walkable());
|
||||||
|
assert!(!Terrain::SoftRock.is_walkable());
|
||||||
|
assert!(!Terrain::HardRock.is_walkable());
|
||||||
|
assert!(!Terrain::Bedrock.is_walkable());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terrain_numbers_are_stable() {
|
||||||
|
for terrain in [
|
||||||
|
Terrain::Ground,
|
||||||
|
Terrain::SoftRock,
|
||||||
|
Terrain::HardRock,
|
||||||
|
Terrain::Bedrock,
|
||||||
|
] {
|
||||||
|
assert_eq!(Terrain::from_u8(terrain as u8), Some(terrain));
|
||||||
|
}
|
||||||
|
assert_eq!(Terrain::from_u8(4), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_map_survives_encoding() {
|
||||||
|
let mut map = Map::filled(5, 4, Terrain::Ground);
|
||||||
|
map.set(
|
||||||
|
Pos::new(2, 1),
|
||||||
|
Tile {
|
||||||
|
terrain: Terrain::SoftRock,
|
||||||
|
damage: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mut w = Writer::new();
|
||||||
|
map.encode(&mut w);
|
||||||
|
let bytes = w.into_bytes();
|
||||||
|
let mut r = Reader::new(&bytes);
|
||||||
|
assert_eq!(Map::decode(&mut r), Ok(map));
|
||||||
|
assert_eq!(r.finish(), Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encoded_map(width: u32, height: u32, tiles: &[(u8, u8)]) -> Vec<u8> {
|
||||||
|
let mut w = Writer::new();
|
||||||
|
w.u32(width);
|
||||||
|
w.u32(height);
|
||||||
|
for (terrain, damage) in tiles {
|
||||||
|
w.u8(*terrain);
|
||||||
|
w.u8(*damage);
|
||||||
|
}
|
||||||
|
w.into_bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decoding_rejects_impossible_maps() {
|
||||||
|
let cases: [(Vec<u8>, DecodeError); 6] = [
|
||||||
|
(
|
||||||
|
encoded_map(0, 4, &[]),
|
||||||
|
DecodeError::InvalidValue("map size"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
encoded_map(2_000, 4, &[]),
|
||||||
|
DecodeError::InvalidValue("map size"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
encoded_map(1, 1, &[(9, 0)]),
|
||||||
|
DecodeError::InvalidValue("terrain"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
encoded_map(1, 1, &[(0, 1)]),
|
||||||
|
DecodeError::InvalidValue("tile damage"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
encoded_map(1, 1, &[(1, 3)]),
|
||||||
|
DecodeError::InvalidValue("tile damage"),
|
||||||
|
),
|
||||||
|
(encoded_map(2, 2, &[(0, 0)]), DecodeError::UnexpectedEnd),
|
||||||
|
];
|
||||||
|
for (bytes, expected) in cases {
|
||||||
|
assert_eq!(Map::decode(&mut Reader::new(&bytes)), Err(expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user