From 1a0be06c3b84b3d90ee7c2fccd71eb03c138cf1a Mon Sep 17 00:00:00 2001 From: Steven Wroblewski Date: Wed, 30 Sep 2026 20:49:27 +0200 Subject: [PATCH] Inventar mit Stapeln MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 24 Plätze mit je bis zu 99 Gegenständen. Hinzufügen füllt zuerst vorhandene Stapel und meldet, was nicht passt; Entfernen nimmt alles oder nichts. Co-Authored-By: Claude Haiku 4.5 --- core/src/inventory.rs | 157 ++++++++++++++++++++++++++++++++++++++++ core/src/lib.rs | 1 + core/tests/inventory.rs | 125 ++++++++++++++++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 core/src/inventory.rs create mode 100644 core/tests/inventory.rs diff --git a/core/src/inventory.rs b/core/src/inventory.rs new file mode 100644 index 0000000..61882f5 --- /dev/null +++ b/core/src/inventory.rs @@ -0,0 +1,157 @@ +//! Item stacks carried by a player. + +use crate::codec::{DecodeError, Reader, Writer}; + +/// Identifies a kind of item. The value zero is reserved for empty slots. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct ItemId(pub u16); + +/// Stone, gained by breaking rock. +pub const ITEM_STONE: ItemId = ItemId(1); + +/// Number of slots in an inventory. +pub const INVENTORY_SLOTS: usize = 24; +/// Largest number of items in one slot. +pub const STACK_LIMIT: u32 = 99; + +/// Items of one kind in one slot. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Stack { + pub item: ItemId, + pub count: u32, +} + +/// Fixed number of slots, each empty or holding one stack. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Inventory { + slots: [Option; INVENTORY_SLOTS], +} + +impl Default for Inventory { + fn default() -> Self { + Self::new() + } +} + +impl Inventory { + pub fn new() -> Self { + Self { + slots: [None; INVENTORY_SLOTS], + } + } + + pub fn slots(&self) -> &[Option] { + &self.slots + } + + /// Total number of `item` across all slots. + pub fn count(&self, item: ItemId) -> u32 { + self.slots + .iter() + .flatten() + .filter(|stack| stack.item == item) + .map(|stack| stack.count) + .sum() + } + + /// How many more of `item` fit. + pub fn free_space(&self, item: ItemId) -> u32 { + self.slots + .iter() + .map(|slot| match slot { + None => STACK_LIMIT, + Some(stack) if stack.item == item => STACK_LIMIT - stack.count, + Some(_) => 0, + }) + .sum() + } + + /// Adds up to `count` of `item`. Existing stacks are topped up before + /// empty slots are used. Returns how many did not fit. + /// + /// # Panics + /// Panics if `item` is the reserved id zero. + pub fn add(&mut self, item: ItemId, count: u32) -> u32 { + assert!(item.0 != 0, "item id zero is reserved for empty slots"); + let mut left = count; + for slot in self.slots.iter_mut() { + if left == 0 { + break; + } + let Some(stack) = slot else { continue }; + if stack.item != item { + continue; + } + let moved = left.min(STACK_LIMIT - stack.count); + stack.count += moved; + left -= moved; + } + for slot in self.slots.iter_mut() { + if left == 0 { + break; + } + if slot.is_none() { + let moved = left.min(STACK_LIMIT); + *slot = Some(Stack { item, count: moved }); + left -= moved; + } + } + left + } + + /// Removes exactly `count` of `item`, emptying the last slots first. + /// Returns `false` and changes nothing if fewer are present. + pub fn remove(&mut self, item: ItemId, count: u32) -> bool { + if self.count(item) < count { + return false; + } + let mut left = count; + for slot in self.slots.iter_mut().rev() { + if left == 0 { + break; + } + let Some(stack) = slot else { continue }; + if stack.item != item { + continue; + } + let moved = left.min(stack.count); + stack.count -= moved; + left -= moved; + if stack.count == 0 { + *slot = None; + } + } + true + } + + pub fn encode(&self, w: &mut Writer) { + for slot in &self.slots { + match slot { + None => w.u16(0), + Some(stack) => { + w.u16(stack.item.0); + w.u32(stack.count); + } + } + } + } + + pub fn decode(r: &mut Reader<'_>) -> Result { + let mut slots = [None; INVENTORY_SLOTS]; + for slot in slots.iter_mut() { + let item = r.u16()?; + if item == 0 { + continue; + } + let count = r.u32()?; + if count == 0 || count > STACK_LIMIT { + return Err(DecodeError::InvalidValue("stack count")); + } + *slot = Some(Stack { + item: ItemId(item), + count, + }); + } + Ok(Self { slots }) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 2396db1..130ead2 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -8,6 +8,7 @@ pub mod codec; pub mod hash; +pub mod inventory; pub mod map; pub mod mapgen; pub mod rng; diff --git a/core/tests/inventory.rs b/core/tests/inventory.rs new file mode 100644 index 0000000..9f1b67b --- /dev/null +++ b/core/tests/inventory.rs @@ -0,0 +1,125 @@ +use grovewright_core::codec::{DecodeError, Reader, Writer}; +use grovewright_core::inventory::{ + Inventory, ItemId, Stack, INVENTORY_SLOTS, ITEM_STONE, STACK_LIMIT, +}; + +const OTHER: ItemId = ItemId(2); +const CAPACITY: u32 = STACK_LIMIT * INVENTORY_SLOTS as u32; + +#[test] +fn a_new_inventory_is_empty() { + let inventory = Inventory::new(); + assert_eq!(inventory.slots().len(), INVENTORY_SLOTS); + assert!(inventory.slots().iter().all(Option::is_none)); + assert_eq!(inventory.count(ITEM_STONE), 0); + assert_eq!(inventory.free_space(ITEM_STONE), CAPACITY); + assert_eq!(Inventory::default(), inventory); +} + +#[test] +fn add_tops_up_a_stack_before_using_an_empty_slot() { + let mut inventory = Inventory::new(); + assert_eq!(inventory.add(ITEM_STONE, 90), 0); + assert_eq!(inventory.add(OTHER, 5), 0); + assert_eq!(inventory.add(ITEM_STONE, 20), 0); + + let stone = |count| { + Some(Stack { + item: ITEM_STONE, + count, + }) + }; + assert_eq!(inventory.slots()[0], stone(99)); + assert_eq!( + inventory.slots()[1], + Some(Stack { + item: OTHER, + count: 5 + }) + ); + assert_eq!(inventory.slots()[2], stone(11)); + assert_eq!(inventory.count(ITEM_STONE), 110); + assert_eq!(inventory.count(OTHER), 5); +} + +#[test] +fn add_reports_what_does_not_fit() { + let mut inventory = Inventory::new(); + assert_eq!(inventory.add(ITEM_STONE, CAPACITY + 7), 7); + assert_eq!(inventory.count(ITEM_STONE), CAPACITY); + assert_eq!(inventory.free_space(ITEM_STONE), 0); + assert_eq!(inventory.free_space(OTHER), 0); + assert_eq!(inventory.add(OTHER, 1), 1); +} + +#[test] +fn free_space_counts_empty_slots_and_matching_stacks() { + let mut inventory = Inventory::new(); + inventory.add(ITEM_STONE, 10); + inventory.add(OTHER, 1); + assert_eq!( + inventory.free_space(ITEM_STONE), + CAPACITY - STACK_LIMIT - 10 + ); + assert_eq!(inventory.free_space(OTHER), CAPACITY - STACK_LIMIT - 1); +} + +#[test] +fn remove_takes_exactly_the_requested_amount() { + let mut inventory = Inventory::new(); + inventory.add(ITEM_STONE, 150); + assert!(inventory.remove(ITEM_STONE, 60)); + assert_eq!(inventory.count(ITEM_STONE), 90); + assert_eq!( + inventory.slots()[0], + Some(Stack { + item: ITEM_STONE, + count: 90 + }) + ); + assert_eq!(inventory.slots()[1], None); +} + +#[test] +fn remove_changes_nothing_when_too_few_are_present() { + let mut inventory = Inventory::new(); + inventory.add(ITEM_STONE, 10); + let before = inventory.clone(); + assert!(!inventory.remove(ITEM_STONE, 11)); + assert!(!inventory.remove(OTHER, 1)); + assert_eq!(inventory, before); +} + +#[test] +#[should_panic(expected = "item id zero is reserved")] +fn the_reserved_item_id_cannot_be_added() { + Inventory::new().add(ItemId(0), 1); +} + +#[test] +fn an_inventory_survives_encoding() { + let mut inventory = Inventory::new(); + inventory.add(ITEM_STONE, 120); + inventory.add(OTHER, 3); + let mut w = Writer::new(); + inventory.encode(&mut w); + let bytes = w.into_bytes(); + let mut r = Reader::new(&bytes); + assert_eq!(Inventory::decode(&mut r), Ok(inventory)); + assert_eq!(r.finish(), Ok(())); +} + +#[test] +fn decoding_rejects_impossible_stack_counts() { + for count in [0, STACK_LIMIT + 1] { + let mut w = Writer::new(); + w.u16(ITEM_STONE.0); + w.u32(count); + let bytes = w.into_bytes(); + assert_eq!( + Inventory::decode(&mut Reader::new(&bytes)), + Err(DecodeError::InvalidValue("stack count")), + "count {count}" + ); + } +}