Karte aus Kacheln

Gelände mit Grabregeln, Kacheln mit Schaden und eine rechteckige Karte,
die sich kodieren lässt und unmögliche Eingaben ablehnt.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-09-30 20:32:06 +02:00
parent 4df328243c
commit 88d8069f8e
3 changed files with 323 additions and 0 deletions
+1
View File
@@ -8,4 +8,5 @@
pub mod codec;
pub mod hash;
pub mod map;
pub mod rng;
+190
View File
@@ -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,
})
}
}