Inventar mit Stapeln
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<Stack>; 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<Stack>] {
|
||||
&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<Self, DecodeError> {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
pub mod codec;
|
||||
pub mod hash;
|
||||
pub mod inventory;
|
||||
pub mod map;
|
||||
pub mod mapgen;
|
||||
pub mod rng;
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user