Files
synthesis/tests/chemistry.test.ts
Денис Шкабатур 7aabb8b4fc Phase 1: Chemistry engine — elements, compounds, reactions
- 20 real elements from periodic table (H through Hg) with accurate data
- 25 compounds with game effects (NaCl, H₂O, gunpowder, thermite, etc.)
- 34 reactions: synthesis, combustion, acid-base, redox, decomposition
- Reaction engine with O(1) lookup by sorted reactant key
- Educational failure reasons (noble gas, missing heat/catalyst, wrong proportions)
- Condition system: temperature, catalyst, energy requirements
- 35 unit tests passing, TypeScript strict, zero errors

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 12:16:08 +03:00

348 lines
12 KiB
TypeScript

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);
}
}
});
});