Files
synthesis/tests/inventory.test.ts
Денис Шкабатур cf36c0adce feat: weight-based inventory with element stacking (Phase 4.2)
Inventory uses real atomic/molecular masses (AMU). Same items auto-stack.
Respects weight limits and slot limits. Supports elements and compounds
via chemistry registries. 28 new tests (162 total).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 13:11:20 +03:00

245 lines
6.9 KiB
TypeScript

/**
* Inventory System Tests — Phase 4.2
*
* Weight-based inventory with element stacking,
* mass limits, and slot limits.
*/
import { describe, it, expect } from 'vitest';
import { Inventory, getItemMass } from '../src/player/inventory';
// === Item Mass Lookup ===
describe('getItemMass', () => {
it('returns atomic mass for elements', () => {
expect(getItemMass('H')).toBeCloseTo(1.008);
expect(getItemMass('Na')).toBeCloseTo(22.99);
expect(getItemMass('Fe')).toBeCloseTo(55.845);
expect(getItemMass('O')).toBeCloseTo(15.999);
});
it('returns molecular mass for compounds', () => {
expect(getItemMass('NaCl')).toBeCloseTo(58.44);
expect(getItemMass('H2O')).toBeCloseTo(18.015);
expect(getItemMass('CO2')).toBeCloseTo(44.01);
});
it('returns 0 for unknown items', () => {
expect(getItemMass('UNKNOWN')).toBe(0);
expect(getItemMass('')).toBe(0);
});
});
// === Inventory Core ===
describe('Inventory — creation', () => {
it('starts empty', () => {
const inv = new Inventory();
expect(inv.isEmpty()).toBe(true);
expect(inv.getItems()).toEqual([]);
expect(inv.getTotalWeight()).toBe(0);
expect(inv.slotCount).toBe(0);
});
it('respects custom limits', () => {
const inv = new Inventory(100, 5);
expect(inv.maxWeight).toBe(100);
expect(inv.maxSlots).toBe(5);
});
});
describe('Inventory — adding items', () => {
it('adds elements', () => {
const inv = new Inventory();
const added = inv.addItem('H', 3);
expect(added).toBe(3);
expect(inv.getCount('H')).toBe(3);
expect(inv.isEmpty()).toBe(false);
expect(inv.slotCount).toBe(1);
});
it('adds compounds', () => {
const inv = new Inventory();
const added = inv.addItem('NaCl', 1);
expect(added).toBe(1);
expect(inv.getCount('NaCl')).toBe(1);
});
it('stacks same elements', () => {
const inv = new Inventory();
inv.addItem('Na', 2);
inv.addItem('Na', 3);
expect(inv.getCount('Na')).toBe(5);
expect(inv.slotCount).toBe(1);
});
it('stacking does not consume new slot', () => {
const inv = new Inventory(10000, 3);
inv.addItem('H', 1);
inv.addItem('O', 1);
inv.addItem('Na', 1);
const added = inv.addItem('H', 5);
expect(added).toBe(5);
expect(inv.getCount('H')).toBe(6);
expect(inv.slotCount).toBe(3);
});
it('returns 0 for unknown items', () => {
const inv = new Inventory();
expect(inv.addItem('UNKNOWN', 1)).toBe(0);
expect(inv.isEmpty()).toBe(true);
});
it('returns 0 for zero/negative count', () => {
const inv = new Inventory();
expect(inv.addItem('H', 0)).toBe(0);
expect(inv.addItem('H', -1)).toBe(0);
});
});
describe('Inventory — removing items', () => {
it('removes items', () => {
const inv = new Inventory();
inv.addItem('H', 5);
const removed = inv.removeItem('H', 3);
expect(removed).toBe(3);
expect(inv.getCount('H')).toBe(2);
});
it('cleans up slot when count reaches zero', () => {
const inv = new Inventory();
inv.addItem('H', 3);
inv.removeItem('H', 3);
expect(inv.getCount('H')).toBe(0);
expect(inv.slotCount).toBe(0);
expect(inv.isEmpty()).toBe(true);
});
it('removes only what is available', () => {
const inv = new Inventory();
inv.addItem('H', 2);
const removed = inv.removeItem('H', 5);
expect(removed).toBe(2);
expect(inv.getCount('H')).toBe(0);
});
it('returns 0 for non-existent item', () => {
const inv = new Inventory();
expect(inv.removeItem('H', 1)).toBe(0);
});
it('returns 0 for zero/negative count', () => {
const inv = new Inventory();
inv.addItem('H', 5);
expect(inv.removeItem('H', 0)).toBe(0);
expect(inv.removeItem('H', -1)).toBe(0);
});
});
describe('Inventory — weight system', () => {
it('calculates total weight from atomic masses', () => {
const inv = new Inventory();
inv.addItem('Na', 2); // 22.99 * 2 = 45.98
expect(inv.getTotalWeight()).toBeCloseTo(22.99 * 2);
});
it('calculates mixed element + compound weight', () => {
const inv = new Inventory();
inv.addItem('Na', 1); // 22.99
inv.addItem('NaCl', 1); // 58.44
expect(inv.getTotalWeight()).toBeCloseTo(22.99 + 58.44);
});
it('limits additions by weight', () => {
const inv = new Inventory(100); // 100 AMU limit
// Na mass = 22.99 → max 4 fit (4 * 22.99 = 91.96)
const added = inv.addItem('Na', 10);
expect(added).toBe(4);
expect(inv.getCount('Na')).toBe(4);
expect(inv.getTotalWeight()).toBeCloseTo(22.99 * 4);
});
it('weight frees up after removal', () => {
const inv = new Inventory(100);
inv.addItem('Na', 4); // 91.96
inv.removeItem('Na', 2); // now 45.98 used, ~54 free
const added = inv.addItem('Na', 3); // 2 more fit (45.98 + 2*22.99 = 91.96)
expect(added).toBe(2);
expect(inv.getCount('Na')).toBe(4);
});
it('getItemWeight returns stack weight', () => {
const inv = new Inventory();
inv.addItem('Na', 3);
expect(inv.getItemWeight('Na')).toBeCloseTo(22.99 * 3);
});
it('getItemWeight returns 0 for absent item', () => {
const inv = new Inventory();
expect(inv.getItemWeight('H')).toBe(0);
});
});
describe('Inventory — slot limits', () => {
it('respects max slots', () => {
const inv = new Inventory(10000, 3);
inv.addItem('H', 1);
inv.addItem('O', 1);
inv.addItem('Na', 1);
const added = inv.addItem('Fe', 1); // 4th slot
expect(added).toBe(0);
expect(inv.slotCount).toBe(3);
expect(inv.hasItem('Fe')).toBe(false);
});
it('frees slot after full removal', () => {
const inv = new Inventory(10000, 2);
inv.addItem('H', 1);
inv.addItem('O', 1);
expect(inv.addItem('Na', 1)).toBe(0); // full
inv.removeItem('O', 1); // free a slot
expect(inv.addItem('Na', 1)).toBe(1); // now fits
expect(inv.slotCount).toBe(2);
});
});
describe('Inventory — queries', () => {
it('hasItem checks against count', () => {
const inv = new Inventory();
inv.addItem('H', 3);
expect(inv.hasItem('H', 1)).toBe(true);
expect(inv.hasItem('H', 3)).toBe(true);
expect(inv.hasItem('H', 4)).toBe(false);
expect(inv.hasItem('O')).toBe(false);
});
it('getItems returns all items', () => {
const inv = new Inventory();
inv.addItem('H', 3);
inv.addItem('Na', 2);
const items = inv.getItems();
expect(items).toHaveLength(2);
expect(items).toContainEqual({ id: 'H', count: 3 });
expect(items).toContainEqual({ id: 'Na', count: 2 });
});
it('getCount returns 0 for absent item', () => {
const inv = new Inventory();
expect(inv.getCount('H')).toBe(0);
});
});
describe('Inventory — clear', () => {
it('removes everything', () => {
const inv = new Inventory();
inv.addItem('H', 3);
inv.addItem('Na', 2);
inv.addItem('NaCl', 1);
inv.clear();
expect(inv.isEmpty()).toBe(true);
expect(inv.getTotalWeight()).toBe(0);
expect(inv.slotCount).toBe(0);
expect(inv.getItems()).toEqual([]);
});
});