import { describe, it, expect } from 'vitest'; import { ElementRegistry } from '../src/chemistry/elements'; import { CompoundRegistry } from '../src/chemistry/compounds'; import { ReactionEngine } from '../src/chemistry/engine'; // ============================================================================= // ELEMENT REGISTRY // ============================================================================= describe('ElementRegistry', () => { it('should load all 20 elements', () => { expect(ElementRegistry.count()).toBe(20); }); it('should look up elements by symbol', () => { const na = ElementRegistry.getBySymbol('Na'); expect(na).toBeDefined(); expect(na!.name).toBe('Sodium'); expect(na!.nameRu).toBe('Натрий'); expect(na!.atomicNumber).toBe(11); expect(na!.atomicMass).toBeCloseTo(22.990, 2); expect(na!.category).toBe('alkali-metal'); }); it('should look up elements by atomic number', () => { const fe = ElementRegistry.getByNumber(26); expect(fe).toBeDefined(); expect(fe!.symbol).toBe('Fe'); expect(fe!.name).toBe('Iron'); }); it('should return undefined for non-existent elements', () => { expect(ElementRegistry.getBySymbol('Xx')).toBeUndefined(); expect(ElementRegistry.getByNumber(999)).toBeUndefined(); }); it('should have correct data for all elements (real periodic table)', () => { const h = ElementRegistry.getBySymbol('H')!; expect(h.atomicNumber).toBe(1); expect(h.atomicMass).toBeCloseTo(1.008, 2); expect(h.state).toBe('gas'); const hg = ElementRegistry.getBySymbol('Hg')!; expect(hg.atomicNumber).toBe(80); expect(hg.state).toBe('liquid'); // Only metal liquid at room temp! const he = ElementRegistry.getBySymbol('He')!; expect(he.category).toBe('noble-gas'); expect(he.electronegativity).toBe(0); const au = ElementRegistry.getBySymbol('Au')!; expect(au.atomicNumber).toBe(79); expect(au.category).toBe('transition-metal'); }); it('should identify elements correctly', () => { expect(ElementRegistry.isElement('Na')).toBe(true); expect(ElementRegistry.isElement('Fe')).toBe(true); expect(ElementRegistry.isElement('NaCl')).toBe(false); expect(ElementRegistry.isElement('H2O')).toBe(false); }); }); // ============================================================================= // COMPOUND REGISTRY // ============================================================================= describe('CompoundRegistry', () => { it('should load all compounds', () => { expect(CompoundRegistry.count()).toBeGreaterThanOrEqual(20); }); it('should look up compounds by id', () => { const water = CompoundRegistry.getById('H2O'); expect(water).toBeDefined(); expect(water!.name).toBe('Water'); expect(water!.formula).toBe('H₂O'); expect(water!.mass).toBeCloseTo(18.015, 2); expect(water!.state).toBe('liquid'); }); it('should have game effects for each compound', () => { const salt = CompoundRegistry.getById('NaCl')!; expect(salt.gameEffects).toContain('preservation'); const gunpowder = CompoundRegistry.getById('GUNPOWDER')!; expect(gunpowder.gameEffects).toContain('explosive'); expect(gunpowder.properties.explosive).toBe(true); }); it('should correctly flag dangerous compounds', () => { const hcl = CompoundRegistry.getById('HCl')!; expect(hcl.properties.acidic).toBe(true); expect(hcl.properties.corrosive).toBe(true); const co = CompoundRegistry.getById('CO')!; expect(co.properties.toxic).toBe(true); const naoh = CompoundRegistry.getById('NaOH')!; expect(naoh.properties.basic).toBe(true); expect(naoh.properties.corrosive).toBe(true); }); it('should identify compounds correctly', () => { expect(CompoundRegistry.isCompound('NaCl')).toBe(true); expect(CompoundRegistry.isCompound('H2O')).toBe(true); expect(CompoundRegistry.isCompound('Na')).toBe(false); }); }); // ============================================================================= // REACTION ENGINE — SUCCESSFUL REACTIONS // ============================================================================= describe('ReactionEngine — success', () => { it('should produce NaCl from Na + Cl', () => { const result = ReactionEngine.react([ { id: 'Na', count: 1 }, { id: 'Cl', count: 1 }, ]); expect(result.success).toBe(true); expect(result.products).toEqual([{ id: 'NaCl', count: 1 }]); }); it('should produce H2O from 2H + O', () => { const result = ReactionEngine.react( [ { id: 'H', count: 2 }, { id: 'O', count: 1 }, ], { minTemp: 500 }, ); expect(result.success).toBe(true); expect(result.products).toEqual([{ id: 'H2O', count: 1 }]); }); it('should produce CO2 from C + 2O with heat', () => { const result = ReactionEngine.react( [ { id: 'C', count: 1 }, { id: 'O', count: 2 }, ], { minTemp: 500 }, ); expect(result.success).toBe(true); expect(result.products).toEqual([{ id: 'CO2', count: 1 }]); expect(result.reaction!.type).toBe('combustion'); }); it('should produce NaOH + H from Na + H2O (violent reaction)', () => { const result = ReactionEngine.react([ { id: 'Na', count: 1 }, { id: 'H2O', count: 1 }, ]); expect(result.success).toBe(true); expect(result.products).toContainEqual({ id: 'NaOH', count: 1 }); expect(result.products).toContainEqual({ id: 'H', count: 1 }); expect(result.reaction!.energyChange).toBeLessThan(-50); // Very exothermic }); it('should produce gunpowder from KNO3 + S + C', () => { const result = ReactionEngine.react([ { id: 'KNO3', count: 1 }, { id: 'S', count: 1 }, { id: 'C', count: 1 }, ]); expect(result.success).toBe(true); expect(result.products).toEqual([{ id: 'GUNPOWDER', count: 1 }]); }); it('should produce thermite: Fe2O3 + 2Al → 2Fe + Al2O3', () => { const result = ReactionEngine.react( [ { id: 'Fe2O3', count: 1 }, { id: 'Al', count: 2 }, ], { minTemp: 1000 }, ); expect(result.success).toBe(true); expect(result.products).toContainEqual({ id: 'Fe', count: 2 }); expect(result.products).toContainEqual({ id: 'Al2O3', count: 1 }); expect(result.reaction!.energyChange).toBe(-100); // Maximum exothermic }); it('should produce NaCl + H2O from NaOH + HCl (neutralization)', () => { const result = ReactionEngine.react([ { id: 'NaOH', count: 1 }, { id: 'HCl', count: 1 }, ]); expect(result.success).toBe(true); expect(result.products).toContainEqual({ id: 'NaCl', count: 1 }); expect(result.products).toContainEqual({ id: 'H2O', count: 1 }); expect(result.reaction!.type).toBe('acid-base'); }); it('should decompose H2O with energy input (electrolysis)', () => { const result = ReactionEngine.react( [{ id: 'H2O', count: 1 }], { requiresEnergy: true }, ); expect(result.success).toBe(true); expect(result.products).toContainEqual({ id: 'H', count: 2 }); expect(result.products).toContainEqual({ id: 'O', count: 1 }); expect(result.reaction!.energyChange).toBeGreaterThan(0); // Endothermic }); it('reactant order should not matter (key is sorted)', () => { const r1 = ReactionEngine.react([ { id: 'Cl', count: 1 }, { id: 'Na', count: 1 }, ]); const r2 = ReactionEngine.react([ { id: 'Na', count: 1 }, { id: 'Cl', count: 1 }, ]); expect(r1.success).toBe(true); expect(r2.success).toBe(true); expect(r1.products).toEqual(r2.products); }); }); // ============================================================================= // REACTION ENGINE — FAILURES (educational) // ============================================================================= describe('ReactionEngine — failures', () => { it('should reject noble gas reactions with explanation', () => { const result = ReactionEngine.react([ { id: 'He', count: 1 }, { id: 'O', count: 1 }, ]); expect(result.success).toBe(false); expect(result.failureReason).toContain('noble gas'); expect(result.failureReasonRu).toContain('благородный газ'); }); it('should reject gold reactions with explanation', () => { const result = ReactionEngine.react([ { id: 'Au', count: 1 }, { id: 'O', count: 1 }, ]); expect(result.success).toBe(false); expect(result.failureReason).toContain('Gold'); }); it('should fail when heat is required but not provided', () => { const result = ReactionEngine.react([ { id: 'C', count: 1 }, { id: 'O', count: 2 }, ]); // No heat expect(result.success).toBe(false); expect(result.failureReason).toContain('temperature'); }); it('should fail when catalyst is required but not provided', () => { const result = ReactionEngine.react( [ { id: 'C', count: 1 }, { id: 'H', count: 4 }, ], { minTemp: 500 }, // Heat provided, but catalyst (Fe) missing ); expect(result.success).toBe(false); expect(result.failureReason).toContain('catalyst'); }); it('should fail when energy is required but not provided', () => { const result = ReactionEngine.react([ { id: 'H2O', count: 1 }, ]); // Electrolysis needs energy expect(result.success).toBe(false); expect(result.failureReason).toContain('energy'); }); it('should fail for unknown substances', () => { const result = ReactionEngine.react([ { id: 'Unobtainium', count: 1 }, { id: 'Na', count: 1 }, ]); expect(result.success).toBe(false); expect(result.failureReason).toContain('Unknown substance'); }); it('should fail for same element with itself', () => { const result = ReactionEngine.react([ { id: 'Fe', count: 1 }, { id: 'Fe', count: 1 }, ]); expect(result.success).toBe(false); expect(result.failureReason).toBeDefined(); }); it('should suggest wrong proportions when elements match but counts differ', () => { // H + O exists? No, H:1+O:1 doesn't but H:2+O:1 does const result = ReactionEngine.react( [ { id: 'H', count: 1 }, { id: 'O', count: 1 }, ], { minTemp: 500 }, ); expect(result.success).toBe(false); expect(result.failureReason).toContain('proportions'); }); }); // ============================================================================= // REACTION ENGINE — METADATA // ============================================================================= describe('ReactionEngine — metadata', () => { it('should have 30+ registered reactions', () => { expect(ReactionEngine.count()).toBeGreaterThanOrEqual(30); }); it('should look up reactions by id', () => { const thermite = ReactionEngine.getById('redox_thermite'); expect(thermite).toBeDefined(); expect(thermite!.type).toBe('redox'); }); it('every reaction should have both English and Russian descriptions', () => { for (const r of ReactionEngine.getAll()) { expect(r.description.length).toBeGreaterThan(10); expect(r.descriptionRu.length).toBeGreaterThan(10); } }); it('every reaction should have valid reactants and products', () => { for (const r of ReactionEngine.getAll()) { expect(r.reactants.length).toBeGreaterThanOrEqual(1); expect(r.products.length).toBeGreaterThanOrEqual(1); for (const reactant of r.reactants) { expect(reactant.count).toBeGreaterThan(0); const exists = ElementRegistry.isElement(reactant.id) || CompoundRegistry.isCompound(reactant.id); expect(exists, `Reactant "${reactant.id}" in reaction "${r.id}" not found`).toBe(true); } for (const product of r.products) { expect(product.count).toBeGreaterThan(0); const exists = ElementRegistry.isElement(product.id) || CompoundRegistry.isCompound(product.id); expect(exists, `Product "${product.id}" in reaction "${r.id}" not found`).toBe(true); } } }); });