craftFromInventory checks reagent availability, runs ReactionEngine, consumes reagents on success, adds products. Failed attempts return educational reasons (noble gas inertness, missing conditions, etc.). 11 new tests (185 total). Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.2 KiB
TypeScript
190 lines
5.2 KiB
TypeScript
/**
|
|
* Crafting System Tests — Phase 4.4
|
|
*
|
|
* Tests: crafting from inventory, reagent consumption,
|
|
* product addition, failure reasons, condition checks.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { Inventory } from '../src/player/inventory';
|
|
import { craftFromInventory, type CraftInput } from '../src/player/crafting';
|
|
|
|
describe('craftFromInventory — success', () => {
|
|
it('crafts NaCl from Na + Cl', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Na', 3);
|
|
inv.addItem('Cl', 2);
|
|
|
|
const inputs: CraftInput[] = [
|
|
{ id: 'Na', count: 1 },
|
|
{ id: 'Cl', count: 1 },
|
|
];
|
|
const result = craftFromInventory(inv, inputs);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.products).toContainEqual({ id: 'NaCl', count: 1 });
|
|
// Reagents consumed
|
|
expect(inv.getCount('Na')).toBe(2);
|
|
expect(inv.getCount('Cl')).toBe(1);
|
|
// Product added
|
|
expect(inv.getCount('NaCl')).toBe(1);
|
|
});
|
|
|
|
it('returns reaction data on success', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Na', 1);
|
|
inv.addItem('Cl', 1);
|
|
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'Na', count: 1 },
|
|
{ id: 'Cl', count: 1 },
|
|
]);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.reaction).toBeDefined();
|
|
expect(result.reaction!.id).toBe('synth_nacl');
|
|
});
|
|
|
|
it('reports consumed reactants', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Na', 5);
|
|
inv.addItem('Cl', 5);
|
|
|
|
const inputs: CraftInput[] = [
|
|
{ id: 'Na', count: 1 },
|
|
{ id: 'Cl', count: 1 },
|
|
];
|
|
const result = craftFromInventory(inv, inputs);
|
|
|
|
expect(result.consumedReactants).toEqual(inputs);
|
|
});
|
|
});
|
|
|
|
describe('craftFromInventory — insufficient materials', () => {
|
|
it('fails when missing reagent', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Na', 1);
|
|
// No Cl in inventory
|
|
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'Na', count: 1 },
|
|
{ id: 'Cl', count: 1 },
|
|
]);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.failureReason).toContain('Cl');
|
|
// Na not consumed
|
|
expect(inv.getCount('Na')).toBe(1);
|
|
});
|
|
|
|
it('fails when not enough of a reagent', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Na', 1);
|
|
inv.addItem('Cl', 1);
|
|
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'Na', count: 5 }, // need 5, have 1
|
|
{ id: 'Cl', count: 1 },
|
|
]);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.failureReason).toContain('Na');
|
|
});
|
|
});
|
|
|
|
describe('craftFromInventory — unknown reaction', () => {
|
|
it('fails with educational reason for no known reaction', () => {
|
|
const inv = new Inventory(5000); // large capacity for heavy elements
|
|
inv.addItem('Fe', 5);
|
|
inv.addItem('Au', 5);
|
|
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'Fe', count: 1 },
|
|
{ id: 'Au', count: 1 },
|
|
]);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.failureReason).toBeDefined();
|
|
expect(result.failureReason!.length).toBeGreaterThan(10); // educational reason
|
|
// Reagents not consumed
|
|
expect(inv.getCount('Fe')).toBe(5);
|
|
expect(inv.getCount('Au')).toBe(5);
|
|
});
|
|
|
|
it('explains noble gas inertness', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('He', 5);
|
|
inv.addItem('O', 5);
|
|
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'He', count: 1 },
|
|
{ id: 'O', count: 1 },
|
|
]);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.failureReason).toContain('noble gas');
|
|
});
|
|
});
|
|
|
|
describe('craftFromInventory — conditions', () => {
|
|
it('fails when temperature requirement not met', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Fe', 5);
|
|
inv.addItem('S', 5);
|
|
|
|
// Fe + S → FeS requires minTemp: 500
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'Fe', count: 1 },
|
|
{ id: 'S', count: 1 },
|
|
]);
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.failureReason).toContain('temperature');
|
|
// Reagents not consumed
|
|
expect(inv.getCount('Fe')).toBe(5);
|
|
});
|
|
|
|
it('succeeds when conditions are met', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Fe', 5);
|
|
inv.addItem('S', 5);
|
|
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'Fe', count: 1 },
|
|
{ id: 'S', count: 1 },
|
|
], { minTemp: 500 });
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.products).toContainEqual({ id: 'FeS', count: 1 });
|
|
expect(inv.getCount('Fe')).toBe(4);
|
|
expect(inv.getCount('S')).toBe(4);
|
|
});
|
|
});
|
|
|
|
describe('craftFromInventory — edge cases', () => {
|
|
it('handles empty reactants list', () => {
|
|
const inv = new Inventory();
|
|
inv.addItem('Na', 5);
|
|
|
|
const result = craftFromInventory(inv, []);
|
|
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('handles inventory full for products', () => {
|
|
const inv = new Inventory(100, 2); // Only 2 slots
|
|
inv.addItem('Na', 1);
|
|
inv.addItem('Cl', 1);
|
|
|
|
// After crafting: Na consumed (slot freed), Cl consumed (slot freed), NaCl added
|
|
// This should work since slots are freed before products are added
|
|
const result = craftFromInventory(inv, [
|
|
{ id: 'Na', count: 1 },
|
|
{ id: 'Cl', count: 1 },
|
|
]);
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(inv.getCount('NaCl')).toBe(1);
|
|
});
|
|
});
|