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>
This commit is contained in:
Денис Шкабатур
2026-02-12 12:16:08 +03:00
parent 10bd67c951
commit 7aabb8b4fc
11 changed files with 1550 additions and 11 deletions

View File

@@ -0,0 +1 @@
[ 7988ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:5173/favicon.ico:0

View File

@@ -1,7 +1,7 @@
# Synthesis — Development Progress
> **Last updated:** 2026-02-12
> **Current phase:** Phase 0 ✅ → Ready for Phase 1
> **Current phase:** Phase 1 ✅ → Ready for Phase 2
---
@@ -19,23 +19,32 @@
- [x] Implementation plan (`IMPLEMENTATION-PLAN.md`)
- [x] Progress tracking (this file)
### Phase 1: Chemistry Engine ✅
- [x] 1.1 Types and interfaces (`src/chemistry/types.ts`)
- [x] 1.2 Element data — 20 real elements (`src/data/elements.json`)
- [x] 1.3 Element registry with lookup by symbol/number (`src/chemistry/elements.ts`)
- [x] 1.4 Reaction engine — O(1) lookup, condition checking, failure reasons (`src/chemistry/engine.ts`)
- [x] 1.5 Reaction data — 34 real reactions (`src/data/reactions.json`)
- [x] 1.6 Compound data — 25 compounds with game effects (`src/data/compounds.json`)
- [x] 1.7 Unit tests — 35 passing (`tests/chemistry.test.ts`)
---
## In Progress
_None — ready to begin Phase 1_
_None — ready to begin Phase 2_
---
## Up Next: Phase 1Chemistry Engine
## Up Next: Phase 2ECS Foundation
- [ ] 1.1 Types and interfaces (`Element`, `Reaction`, `Compound`)
- [ ] 1.2 Element data — 20 real elements (JSON)
- [ ] 1.3 Element registry with lookup
- [ ] 1.4 Reaction engine core
- [ ] 1.5 Reaction data — 50 real reactions (JSON)
- [ ] 1.6 Compound properties
- [ ] 1.7 Unit tests (vitest)
- [ ] 2.1 World setup (bitECS world + time tracking)
- [ ] 2.2 Core components (Position, Velocity, SpriteRef, Health)
- [ ] 2.3 Movement system
- [ ] 2.4 Phaser ↔ bitECS sync bridge
- [ ] 2.5 Entity factory
- [ ] 2.6 Health/damage system
- [ ] 2.7 Visual test (entities moving on screen)
---
@@ -49,4 +58,5 @@ None
| # | Date | Phase | Summary |
|---|------|-------|---------|
| 1 | 2026-02-12 | Phase 0 | Project setup complete: GDD, engine analysis, npm init, Phaser config, BootScene, cursor rules, implementation plan |
| 1 | 2026-02-12 | Phase 0 | Project setup: GDD, engine analysis, npm init, Phaser config, BootScene, cursor rules, plan |
| 2 | 2026-02-12 | Phase 1 | Chemistry engine: 20 elements, 25 compounds, 34 reactions, engine with O(1) lookup + educational failures, 35 tests passing |

BIN
boot-screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,35 @@
import type { CompoundData } from './types';
import compoundsRaw from '../data/compounds.json';
const compounds: CompoundData[] = compoundsRaw as CompoundData[];
const byId = new Map<string, CompoundData>();
for (const c of compounds) {
if (byId.has(c.id)) {
throw new Error(`Duplicate compound id: ${c.id}`);
}
byId.set(c.id, c);
}
export const CompoundRegistry = {
getById(id: string): CompoundData | undefined {
return byId.get(id);
},
getAll(): readonly CompoundData[] {
return compounds;
},
has(id: string): boolean {
return byId.has(id);
},
count(): number {
return compounds.length;
},
isCompound(id: string): boolean {
return byId.has(id);
},
} as const;

45
src/chemistry/elements.ts Normal file
View File

@@ -0,0 +1,45 @@
import type { ElementData } from './types';
import elementsRaw from '../data/elements.json';
const elements: ElementData[] = elementsRaw as ElementData[];
const bySymbol = new Map<string, ElementData>();
const byNumber = new Map<number, ElementData>();
for (const el of elements) {
if (bySymbol.has(el.symbol)) {
throw new Error(`Duplicate element symbol: ${el.symbol}`);
}
if (byNumber.has(el.atomicNumber)) {
throw new Error(`Duplicate atomic number: ${el.atomicNumber}`);
}
bySymbol.set(el.symbol, el);
byNumber.set(el.atomicNumber, el);
}
export const ElementRegistry = {
getBySymbol(symbol: string): ElementData | undefined {
return bySymbol.get(symbol);
},
getByNumber(num: number): ElementData | undefined {
return byNumber.get(num);
},
getAll(): readonly ElementData[] {
return elements;
},
has(symbol: string): boolean {
return bySymbol.has(symbol);
},
count(): number {
return elements.length;
},
/** Check if a symbol is an element (vs compound) */
isElement(id: string): boolean {
return bySymbol.has(id);
},
} as const;

209
src/chemistry/engine.ts Normal file
View File

@@ -0,0 +1,209 @@
import type { Reactant, ReactionData, ReactionResult, ReactionConditions } from './types';
import { ElementRegistry } from './elements';
import { CompoundRegistry } from './compounds';
import reactionsRaw from '../data/reactions.json';
const reactions: ReactionData[] = reactionsRaw as ReactionData[];
// === Reaction Key: sorted "id:count" pairs joined by "+" ===
function makeReactionKey(reactants: Reactant[]): string {
return reactants
.map((r) => `${r.id}:${r.count}`)
.sort()
.join('+');
}
// Build index for O(1) lookup
const reactionIndex = new Map<string, ReactionData>();
for (const r of reactions) {
const key = makeReactionKey(r.reactants);
if (reactionIndex.has(key)) {
console.warn(`Duplicate reaction key: ${key} (${r.id} conflicts with ${reactionIndex.get(key)!.id})`);
}
reactionIndex.set(key, r);
}
// === Failure Reason Generation ===
function isNobleGas(id: string): boolean {
const el = ElementRegistry.getBySymbol(id);
return el?.category === 'noble-gas';
}
function generateFailureReason(
reactants: Reactant[],
conditions?: Partial<ReactionConditions>,
): { reason: string; reasonRu: string } {
const ids = reactants.map((r) => r.id);
// Check for noble gas
const nobleGas = ids.find((id) => isNobleGas(id));
if (nobleGas) {
const el = ElementRegistry.getBySymbol(nobleGas)!;
return {
reason: `${el.name} is a noble gas — it has a full electron shell and refuses to react with anything.`,
reasonRu: `${el.nameRu} — благородный газ с полной электронной оболочкой. Не реагирует ни с чем.`,
};
}
// Check if all inputs are the same element
const uniqueIds = new Set(ids);
if (uniqueIds.size === 1) {
return {
reason: `Cannot react an element with itself under these conditions. Try combining with a different element.`,
reasonRu: `Нельзя провести реакцию элемента с самим собой в этих условиях. Попробуйте другой элемент.`,
};
}
// Check for gold (extremely unreactive)
if (ids.includes('Au')) {
return {
reason: `Gold is extremely unreactive — it resists most chemical attacks. Only aqua regia (HNO₃ + HCl) can dissolve it.`,
reasonRu: `Золото крайне инертно — устойчиво к большинству реагентов. Только царская водка (HNO₃ + HCl) растворяет его.`,
};
}
// Check if a matching reaction exists but conditions aren't met
const key = makeReactionKey(reactants);
// Try nearby keys (different counts)
for (const [rKey, reaction] of reactionIndex) {
const rIds = new Set(reaction.reactants.map((r) => r.id));
const inputIds = new Set(ids);
if (setsEqual(rIds, inputIds) && rKey !== key) {
return {
reason: `These elements can react, but you need different proportions. Check the amounts carefully.`,
reasonRu: `Эти элементы могут реагировать, но нужны другие пропорции. Проверьте количества.`,
};
}
}
// Default
return {
reason: `No known reaction between these substances under current conditions. Try adding heat, a catalyst, or different elements.`,
reasonRu: `Нет известной реакции между этими веществами в текущих условиях. Попробуйте нагрев, катализатор или другие элементы.`,
};
}
function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
if (a.size !== b.size) return false;
for (const item of a) {
if (!b.has(item)) return false;
}
return true;
}
// === Condition Checking ===
function checkConditions(
required: ReactionConditions | undefined,
provided: Partial<ReactionConditions> | undefined,
): { met: boolean; reason?: string; reasonRu?: string } {
if (!required) return { met: true };
if (required.minTemp && (!provided?.minTemp || provided.minTemp < required.minTemp)) {
return {
met: false,
reason: `This reaction requires a temperature of at least ${required.minTemp}°. You need a heat source.`,
reasonRu: `Эта реакция требует температуру не менее ${required.minTemp}°. Нужен источник тепла.`,
};
}
if (required.catalyst) {
const catalystName =
ElementRegistry.getBySymbol(required.catalyst)?.name ??
CompoundRegistry.getById(required.catalyst)?.name ??
required.catalyst;
if (!provided?.catalyst || provided.catalyst !== required.catalyst) {
return {
met: false,
reason: `This reaction requires a catalyst: ${catalystName}. The catalyst is not consumed — it just enables the reaction.`,
reasonRu: `Эта реакция требует катализатор: ${catalystName}. Катализатор не расходуется — он лишь запускает реакцию.`,
};
}
}
if (required.requiresEnergy && !provided?.requiresEnergy) {
return {
met: false,
reason: `This reaction requires an external energy source (e.g., electricity). It's endothermic — it absorbs energy.`,
reasonRu: `Эта реакция требует внешний источник энергии (напр., электричество). Она эндотермическая — поглощает энергию.`,
};
}
return { met: true };
}
// === Public API ===
export const ReactionEngine = {
/**
* Attempt a reaction with given reactants and conditions.
* Returns success with products, or failure with educational reason.
*/
react(
reactants: Reactant[],
conditions?: Partial<ReactionConditions>,
): ReactionResult {
// Validate all inputs exist
for (const r of reactants) {
if (!ElementRegistry.isElement(r.id) && !CompoundRegistry.isCompound(r.id)) {
return {
success: false,
failureReason: `Unknown substance: "${r.id}". Check spelling.`,
failureReasonRu: `Неизвестное вещество: "${r.id}". Проверьте написание.`,
};
}
}
const key = makeReactionKey(reactants);
const reaction = reactionIndex.get(key);
if (!reaction) {
const { reason, reasonRu } = generateFailureReason(reactants, conditions);
return {
success: false,
failureReason: reason,
failureReasonRu: reasonRu,
};
}
// Check conditions
const condCheck = checkConditions(reaction.conditions, conditions);
if (!condCheck.met) {
return {
success: false,
failureReason: condCheck.reason,
failureReasonRu: condCheck.reasonRu,
};
}
return {
success: true,
products: reaction.products,
reaction,
};
},
/** Get a reaction by its ID */
getById(id: string): ReactionData | undefined {
return reactions.find((r) => r.id === id);
},
/** Get all known reactions */
getAll(): readonly ReactionData[] {
return reactions;
},
/** Get the number of registered reactions */
count(): number {
return reactions.length;
},
/** Build a reaction key from reactants (for testing/debugging) */
makeKey(reactants: Reactant[]): string {
return makeReactionKey(reactants);
},
} as const;

102
src/chemistry/types.ts Normal file
View File

@@ -0,0 +1,102 @@
// === Element Types ===
export type ElementCategory =
| 'alkali-metal'
| 'alkaline-earth'
| 'transition-metal'
| 'post-transition-metal'
| 'metalloid'
| 'nonmetal'
| 'halogen'
| 'noble-gas';
export type MatterState = 'solid' | 'liquid' | 'gas';
export interface ElementData {
symbol: string;
name: string;
nameRu: string;
atomicNumber: number;
atomicMass: number;
electronegativity: number; // 0 for noble gases
category: ElementCategory;
state: MatterState; // at room temperature
color: string; // hex for rendering
description: string;
descriptionRu: string;
}
// === Compound Types ===
export interface CompoundData {
id: string; // lookup key, e.g. "NaCl"
formula: string; // display formula with Unicode, e.g. "NaCl"
name: string;
nameRu: string;
mass: number; // molecular mass
state: MatterState;
color: string;
properties: CompoundProperties;
description: string;
descriptionRu: string;
gameEffects: string[];
}
export interface CompoundProperties {
flammable: boolean;
toxic: boolean;
explosive: boolean;
acidic: boolean;
basic: boolean;
oxidizer: boolean;
corrosive: boolean;
}
// === Reaction Types ===
export type ReactionType =
| 'synthesis'
| 'decomposition'
| 'combustion'
| 'single-replacement'
| 'double-replacement'
| 'acid-base'
| 'redox';
export interface Reactant {
id: string; // element symbol or compound id
count: number;
}
export interface Product {
id: string; // element symbol or compound id
count: number;
}
export interface ReactionConditions {
minTemp?: number; // 0=room, 100=boiling, 500=fire, 1000=furnace
catalyst?: string; // compound id required as catalyst
requiresEnergy?: boolean; // needs external energy (electricity)
}
export interface ReactionData {
id: string;
reactants: Reactant[];
products: Product[];
conditions?: ReactionConditions;
energyChange: number; // -100..+100, negative = exothermic
type: ReactionType;
description: string;
descriptionRu: string;
difficulty: number; // 1-5
}
// === Engine Result ===
export interface ReactionResult {
success: boolean;
products?: Product[];
reaction?: ReactionData;
failureReason?: string;
failureReasonRu?: string;
}

202
src/data/compounds.json Normal file
View File

@@ -0,0 +1,202 @@
[
{
"id": "H2O", "formula": "H₂O", "name": "Water", "nameRu": "Вода",
"mass": 18.015, "state": "liquid", "color": "#4488cc",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Universal solvent. Essential for life and countless reactions.",
"descriptionRu": "Универсальный растворитель. Необходима для жизни и бесчисленных реакций.",
"gameEffects": ["hydration", "cooling", "solvent"]
},
{
"id": "NaCl", "formula": "NaCl", "name": "Sodium Chloride", "nameRu": "Поваренная соль",
"mass": 58.44, "state": "solid", "color": "#f0f0f0",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Table salt. Preserves food via osmosis. Essential electrolyte.",
"descriptionRu": "Поваренная соль. Консервирует пищу через осмос. Необходимый электролит.",
"gameEffects": ["preservation", "trade", "slug_repellent"]
},
{
"id": "CO2", "formula": "CO₂", "name": "Carbon Dioxide", "nameRu": "Углекислый газ",
"mass": 44.01, "state": "gas", "color": "#aaaaaa",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": true, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Product of combustion and respiration. Denser than air — extinguishes fire.",
"descriptionRu": "Продукт горения и дыхания. Тяжелее воздуха — тушит огонь.",
"gameEffects": ["fire_extinguisher", "suffocant"]
},
{
"id": "CO", "formula": "CO", "name": "Carbon Monoxide", "nameRu": "Угарный газ",
"mass": 28.01, "state": "gas", "color": "#cccccc",
"properties": { "flammable": true, "toxic": true, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Odorless, deadly poison. Binds to hemoglobin 200x stronger than oxygen.",
"descriptionRu": "Без запаха, смертельно ядовит. Связывается с гемоглобином в 200 раз сильнее кислорода.",
"gameEffects": ["poison_gas", "fuel"]
},
{
"id": "HCl", "formula": "HCl", "name": "Hydrochloric Acid", "nameRu": "Соляная кислота",
"mass": 36.46, "state": "liquid", "color": "#ccff00",
"properties": { "flammable": false, "toxic": true, "explosive": false, "acidic": true, "basic": false, "oxidizer": false, "corrosive": true },
"description": "Strong acid. Dissolves metals and limestone. Present in stomach acid.",
"descriptionRu": "Сильная кислота. Растворяет металлы и известняк. Содержится в желудочном соке.",
"gameEffects": ["dissolve_metal", "dissolve_stone", "damage"]
},
{
"id": "NaOH", "formula": "NaOH", "name": "Sodium Hydroxide", "nameRu": "Гидроксид натрия",
"mass": 40.00, "state": "solid", "color": "#f5f5f5",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": true, "oxidizer": false, "corrosive": true },
"description": "Lye. Extremely caustic. Used for soap making and as a powerful cleaning agent.",
"descriptionRu": "Щёлочь. Крайне едкий. Используется для мыловарения и как мощное чистящее средство.",
"gameEffects": ["soap_making", "corrosive_weapon", "cleaning"]
},
{
"id": "KOH", "formula": "KOH", "name": "Potassium Hydroxide", "nameRu": "Гидроксид калия",
"mass": 56.11, "state": "solid", "color": "#f0f0f0",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": true, "oxidizer": false, "corrosive": true },
"description": "Caustic potash. Even stronger base than NaOH. Key in fertilizer production.",
"descriptionRu": "Едкое кали. Ещё более сильное основание, чем NaOH. Ключ к производству удобрений.",
"gameEffects": ["corrosive_weapon", "fertilizer_base"]
},
{
"id": "CaO", "formula": "CaO", "name": "Calcium Oxide", "nameRu": "Негашёная известь",
"mass": 56.08, "state": "solid", "color": "#f5f5dc",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": true, "oxidizer": false, "corrosive": true },
"description": "Quickite. Reacts violently with water, generating intense heat. Building material.",
"descriptionRu": "Негашёная известь. Бурно реагирует с водой, выделяя сильное тепло. Строительный материал.",
"gameEffects": ["heat_source", "building", "disinfectant"]
},
{
"id": "CaOH2", "formula": "Ca(OH)₂", "name": "Calcium Hydroxide", "nameRu": "Гашёная известь",
"mass": 74.09, "state": "solid", "color": "#f5f5f0",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": true, "oxidizer": false, "corrosive": false },
"description": "Slaked lime. Used in mortar, plaster, and water purification.",
"descriptionRu": "Гашёная известь. Используется в строительном растворе, штукатурке и очистке воды.",
"gameEffects": ["building_mortar", "water_purification", "disinfectant"]
},
{
"id": "MgO", "formula": "MgO", "name": "Magnesium Oxide", "nameRu": "Оксид магния",
"mass": 40.30, "state": "solid", "color": "#ffffff",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": true, "oxidizer": false, "corrosive": false },
"description": "Produced by burning magnesium with a blinding white flash. Refractory material.",
"descriptionRu": "Образуется при горении магния с ослепительной белой вспышкой. Огнеупорный материал.",
"gameEffects": ["flash_blind", "refractory"]
},
{
"id": "Fe2O3", "formula": "Fe₂O₃", "name": "Iron(III) Oxide", "nameRu": "Оксид железа (ржавчина)",
"mass": 159.69, "state": "solid", "color": "#993300",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Rust. Indicator of aging metal. Key ingredient for thermite (with aluminum).",
"descriptionRu": "Ржавчина. Индикатор старения металла. Ключевой ингредиент термита (с алюминием).",
"gameEffects": ["thermite_ingredient", "pigment"]
},
{
"id": "SiO2", "formula": "SiO₂", "name": "Silicon Dioxide", "nameRu": "Диоксид кремния",
"mass": 60.08, "state": "solid", "color": "#e8e8e8",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Silica. Main component of sand, quartz, and glass. Extremely hard.",
"descriptionRu": "Кремнезём. Основной компонент песка, кварца и стекла. Крайне твёрдый.",
"gameEffects": ["glass_making", "abrasive", "building"]
},
{
"id": "FeS", "formula": "FeS", "name": "Iron Sulfide", "nameRu": "Сульфид железа",
"mass": 87.91, "state": "solid", "color": "#b8a000",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Fool's gold (pyrite). Sparks when struck — natural fire starter.",
"descriptionRu": "Пирит (золото дураков). Искрит при ударе — природное огниво.",
"gameEffects": ["fire_starter", "trade_decoy"]
},
{
"id": "ZnS", "formula": "ZnS", "name": "Zinc Sulfide", "nameRu": "Сульфид цинка",
"mass": 97.47, "state": "solid", "color": "#eeffdd",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Phosphorescent — glows in the dark after exposure to light. Natural night-light.",
"descriptionRu": "Фосфоресцирует — светится в темноте после облучения светом. Природный ночник.",
"gameEffects": ["glow", "cave_light", "marking"]
},
{
"id": "CuS", "formula": "CuS", "name": "Copper Sulfide", "nameRu": "Сульфид меди",
"mass": 95.61, "state": "solid", "color": "#1a1a2e",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Dark mineral. Copper ore found near volcanic activity.",
"descriptionRu": "Тёмный минерал. Медная руда, встречающаяся вблизи вулканической активности.",
"gameEffects": ["pigment", "copper_source"]
},
{
"id": "KCl", "formula": "KCl", "name": "Potassium Chloride", "nameRu": "Хлорид калия",
"mass": 74.55, "state": "solid", "color": "#f0f0f0",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Potash salt. Essential plant fertilizer. Salt substitute for food.",
"descriptionRu": "Калийная соль. Необходимое удобрение для растений. Заменитель соли в пище.",
"gameEffects": ["fertilizer", "preservation"]
},
{
"id": "SO2", "formula": "SO₂", "name": "Sulfur Dioxide", "nameRu": "Диоксид серы",
"mass": 64.07, "state": "gas", "color": "#cccc00",
"properties": { "flammable": false, "toxic": true, "explosive": false, "acidic": true, "basic": false, "oxidizer": false, "corrosive": true },
"description": "Pungent, suffocating gas. Fumigant and preservative. Causes acid rain.",
"descriptionRu": "Едкий, удушливый газ. Фумигант и консервант. Вызывает кислотные дожди.",
"gameEffects": ["fumigant", "area_denial", "preservation"]
},
{
"id": "CaCO3", "formula": "CaCO₃", "name": "Calcium Carbonate", "nameRu": "Карбонат кальция",
"mass": 100.09, "state": "solid", "color": "#f5f0e0",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": true, "oxidizer": false, "corrosive": false },
"description": "Limestone, chalk, marble. Building material. Neutralizes acid soil and acid attacks.",
"descriptionRu": "Известняк, мел, мрамор. Строительный материал. Нейтрализует кислую почву и кислотные атаки.",
"gameEffects": ["building", "acid_neutralizer", "soil_amendment"]
},
{
"id": "NaHCO3", "formula": "NaHCO₃", "name": "Sodium Bicarbonate", "nameRu": "Пищевая сода",
"mass": 84.01, "state": "solid", "color": "#ffffff",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": true, "oxidizer": false, "corrosive": false },
"description": "Baking soda. Neutralizes acids, extinguishes grease fires, gentle cleaning agent.",
"descriptionRu": "Пищевая сода. Нейтрализует кислоты, тушит жировые пожары, мягкое чистящее средство.",
"gameEffects": ["acid_neutralizer", "fire_extinguisher", "baking"]
},
{
"id": "KNO3", "formula": "KNO₃", "name": "Potassium Nitrate", "nameRu": "Калиевая селитра",
"mass": 101.10, "state": "solid", "color": "#f0f0f0",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": true, "corrosive": false },
"description": "Saltpeter. Powerful oxidizer. Key component of black gunpowder (with sulfur and charcoal).",
"descriptionRu": "Селитра. Мощный окислитель. Ключевой компонент чёрного пороха (с серой и углём).",
"gameEffects": ["gunpowder_ingredient", "oxidizer", "fertilizer"]
},
{
"id": "ZnO", "formula": "ZnO", "name": "Zinc Oxide", "nameRu": "Оксид цинка",
"mass": 81.38, "state": "solid", "color": "#ffffff",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "White powder. UV protection, wound healing, anti-inflammatory. Used in sunscreen and ointments.",
"descriptionRu": "Белый порошок. Защита от УФ, заживление ран, противовоспалительное. Основа мазей.",
"gameEffects": ["healing_salve", "sun_protection"]
},
{
"id": "Al2O3", "formula": "Al₂O₃", "name": "Aluminum Oxide", "nameRu": "Оксид алюминия",
"mass": 101.96, "state": "solid", "color": "#f0f0f0",
"properties": { "flammable": false, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Corundum. Extremely hard (9 on Mohs scale). Rubies and sapphires are colored alumina.",
"descriptionRu": "Корунд. Крайне твёрдый (9 по шкале Мооса). Рубины и сапфиры — окрашенный корунд.",
"gameEffects": ["abrasive", "armor_material", "gemstone"]
},
{
"id": "CH4", "formula": "CH₄", "name": "Methane", "nameRu": "Метан",
"mass": 16.04, "state": "gas", "color": "#aaddff",
"properties": { "flammable": true, "toxic": false, "explosive": true, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Simplest hydrocarbon. Odorless, highly flammable. Main component of natural gas.",
"descriptionRu": "Простейший углеводород. Без запаха, легковоспламеняем. Основной компонент природного газа.",
"gameEffects": ["fuel", "explosive_gas", "lantern_fuel"]
},
{
"id": "C2H5OH", "formula": "C₂H₅OH", "name": "Ethanol", "nameRu": "Этанол",
"mass": 46.07, "state": "liquid", "color": "#eeeeff",
"properties": { "flammable": true, "toxic": false, "explosive": false, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Drinking alcohol. Disinfectant, fuel, solvent. Intoxicates NPCs.",
"descriptionRu": "Питьевой спирт. Дезинфектант, топливо, растворитель. Опьяняет NPC.",
"gameEffects": ["disinfectant", "fuel", "intoxicant", "solvent"]
},
{
"id": "GUNPOWDER", "formula": "KNO₃+S+C", "name": "Black Gunpowder", "nameRu": "Чёрный порох",
"mass": 165.17, "state": "solid", "color": "#333333",
"properties": { "flammable": true, "toxic": false, "explosive": true, "acidic": false, "basic": false, "oxidizer": false, "corrosive": false },
"description": "Explosive mixture of saltpeter, sulfur, and charcoal. The discovery that changed civilizations.",
"descriptionRu": "Взрывчатая смесь селитры, серы и угля. Открытие, изменившее цивилизации.",
"gameEffects": ["explosive", "propellant", "signal_flare"]
}
]

262
src/data/elements.json Normal file
View File

@@ -0,0 +1,262 @@
[
{
"symbol": "H",
"name": "Hydrogen",
"nameRu": "Водород",
"atomicNumber": 1,
"atomicMass": 1.008,
"electronegativity": 2.20,
"category": "nonmetal",
"state": "gas",
"color": "#ffffff",
"description": "Lightest element. Highly flammable gas. The most abundant element in the universe.",
"descriptionRu": "Легчайший элемент. Легковоспламеняемый газ. Самый распространённый элемент во вселенной."
},
{
"symbol": "He",
"name": "Helium",
"nameRu": "Гелий",
"atomicNumber": 2,
"atomicMass": 4.003,
"electronegativity": 0,
"category": "noble-gas",
"state": "gas",
"color": "#d9ffff",
"description": "Noble gas. Completely inert — refuses to react with anything. Lighter than air.",
"descriptionRu": "Благородный газ. Абсолютно инертен — не реагирует ни с чем. Легче воздуха."
},
{
"symbol": "C",
"name": "Carbon",
"nameRu": "Углерод",
"atomicNumber": 6,
"atomicMass": 12.011,
"electronegativity": 2.55,
"category": "nonmetal",
"state": "solid",
"color": "#333333",
"description": "Basis of all organic chemistry. Forms more compounds than any other element. Burns in oxygen.",
"descriptionRu": "Основа всей органической химии. Образует больше соединений, чем любой другой элемент. Горит в кислороде."
},
{
"symbol": "N",
"name": "Nitrogen",
"nameRu": "Азот",
"atomicNumber": 7,
"atomicMass": 14.007,
"electronegativity": 3.04,
"category": "nonmetal",
"state": "gas",
"color": "#3050f8",
"description": "Makes up 78% of air. Very stable — triple bond is hard to break. Essential for life (proteins, DNA).",
"descriptionRu": "Составляет 78% воздуха. Очень стабилен — тройную связь трудно разорвать. Необходим для жизни (белки, ДНК)."
},
{
"symbol": "O",
"name": "Oxygen",
"nameRu": "Кислород",
"atomicNumber": 8,
"atomicMass": 15.999,
"electronegativity": 3.44,
"category": "nonmetal",
"state": "gas",
"color": "#ff0d0d",
"description": "Essential for combustion and respiration. Highly reactive oxidizer. 21% of air.",
"descriptionRu": "Необходим для горения и дыхания. Сильный окислитель. 21% воздуха."
},
{
"symbol": "Na",
"name": "Sodium",
"nameRu": "Натрий",
"atomicNumber": 11,
"atomicMass": 22.990,
"electronegativity": 0.93,
"category": "alkali-metal",
"state": "solid",
"color": "#c8c8c8",
"description": "Soft alkali metal. Reacts violently with water — explosion and flame! Store away from moisture.",
"descriptionRu": "Мягкий щелочной металл. Бурно реагирует с водой — взрыв и пламя! Хранить вдали от влаги."
},
{
"symbol": "Mg",
"name": "Magnesium",
"nameRu": "Магний",
"atomicNumber": 12,
"atomicMass": 24.305,
"electronegativity": 1.31,
"category": "alkaline-earth",
"state": "solid",
"color": "#8aff00",
"description": "Burns with an intensely bright white flame. Used in flares and incendiary devices.",
"descriptionRu": "Горит ослепительно ярким белым пламенем. Используется в сигнальных ракетах и зажигательных устройствах."
},
{
"symbol": "Al",
"name": "Aluminum",
"nameRu": "Алюминий",
"atomicNumber": 13,
"atomicMass": 26.982,
"electronegativity": 1.61,
"category": "post-transition-metal",
"state": "solid",
"color": "#bfa6a6",
"description": "Light, strong metal. Oxide layer protects from corrosion. Key ingredient in thermite.",
"descriptionRu": "Лёгкий, прочный металл. Оксидная плёнка защищает от коррозии. Ключевой ингредиент термита."
},
{
"symbol": "Si",
"name": "Silicon",
"nameRu": "Кремний",
"atomicNumber": 14,
"atomicMass": 28.086,
"electronegativity": 1.90,
"category": "metalloid",
"state": "solid",
"color": "#f0c8a0",
"description": "Semiconductor. Basis of glass, ceramics, and electronics. Second most abundant in Earth's crust.",
"descriptionRu": "Полупроводник. Основа стекла, керамики и электроники. Второй по распространённости в земной коре."
},
{
"symbol": "P",
"name": "Phosphorus",
"nameRu": "Фосфор",
"atomicNumber": 15,
"atomicMass": 30.974,
"electronegativity": 2.19,
"category": "nonmetal",
"state": "solid",
"color": "#ff8000",
"description": "White phosphorus glows in the dark and ignites spontaneously. Essential for ATP (biological energy).",
"descriptionRu": "Белый фосфор светится в темноте и самовоспламеняется. Необходим для АТФ (биологическая энергия)."
},
{
"symbol": "S",
"name": "Sulfur",
"nameRu": "Сера",
"atomicNumber": 16,
"atomicMass": 32.065,
"electronegativity": 2.58,
"category": "nonmetal",
"state": "solid",
"color": "#ffff30",
"description": "Yellow solid with a distinctive smell. Burns with blue flame. Component of gunpowder.",
"descriptionRu": "Жёлтое твёрдое вещество с характерным запахом. Горит синим пламенем. Компонент пороха."
},
{
"symbol": "Cl",
"name": "Chlorine",
"nameRu": "Хлор",
"atomicNumber": 17,
"atomicMass": 35.453,
"electronegativity": 3.16,
"category": "halogen",
"state": "gas",
"color": "#1ff01f",
"description": "Toxic yellow-green gas. Powerful disinfectant. Combines readily with metals to form salts.",
"descriptionRu": "Ядовитый жёлто-зелёный газ. Мощный дезинфектант. Легко соединяется с металлами, образуя соли."
},
{
"symbol": "K",
"name": "Potassium",
"nameRu": "Калий",
"atomicNumber": 19,
"atomicMass": 39.098,
"electronegativity": 0.82,
"category": "alkali-metal",
"state": "solid",
"color": "#8f40d4",
"description": "Even more reactive than sodium with water — violent purple-flame explosion. Essential nutrient for plants.",
"descriptionRu": "Ещё активнее натрия при контакте с водой — бурный взрыв с фиолетовым пламенем. Необходим растениям."
},
{
"symbol": "Ca",
"name": "Calcium",
"nameRu": "Кальций",
"atomicNumber": 20,
"atomicMass": 40.078,
"electronegativity": 1.00,
"category": "alkaline-earth",
"state": "solid",
"color": "#e0e0e0",
"description": "Essential for bones and shells. Reacts with water, but less violently than sodium. Component of limestone and cement.",
"descriptionRu": "Необходим для костей и раковин. Реагирует с водой, но менее бурно, чем натрий. Компонент известняка и цемента."
},
{
"symbol": "Fe",
"name": "Iron",
"nameRu": "Железо",
"atomicNumber": 26,
"atomicMass": 55.845,
"electronegativity": 1.83,
"category": "transition-metal",
"state": "solid",
"color": "#a0a0a0",
"description": "Strong, abundant metal. Rusts in moist air. Core of Earth is mostly iron. Magnetic.",
"descriptionRu": "Прочный, распространённый металл. Ржавеет на влажном воздухе. Ядро Земли в основном из железа. Магнитен."
},
{
"symbol": "Cu",
"name": "Copper",
"nameRu": "Медь",
"atomicNumber": 29,
"atomicMass": 63.546,
"electronegativity": 1.90,
"category": "transition-metal",
"state": "solid",
"color": "#c88033",
"description": "Excellent conductor of electricity and heat. Turns green (patina) over time. Antibacterial properties.",
"descriptionRu": "Отличный проводник электричества и тепла. Зеленеет (патина) со временем. Антибактериальные свойства."
},
{
"symbol": "Zn",
"name": "Zinc",
"nameRu": "Цинк",
"atomicNumber": 30,
"atomicMass": 65.38,
"electronegativity": 1.65,
"category": "transition-metal",
"state": "solid",
"color": "#7d80b0",
"description": "Protects iron from rusting (galvanization). Zinc sulfide glows under UV light. Essential trace nutrient.",
"descriptionRu": "Защищает железо от ржавчины (гальванизация). Сульфид цинка светится в УФ-свете. Необходимый микроэлемент."
},
{
"symbol": "Sn",
"name": "Tin",
"nameRu": "Олово",
"atomicNumber": 50,
"atomicMass": 118.71,
"electronegativity": 1.96,
"category": "post-transition-metal",
"state": "solid",
"color": "#668080",
"description": "Soft, malleable metal. Resists corrosion. Used for solder and tin plating. Alloy with copper makes bronze.",
"descriptionRu": "Мягкий, ковкий металл. Устойчив к коррозии. Используется для пайки и лужения. Сплав с медью — бронза."
},
{
"symbol": "Au",
"name": "Gold",
"nameRu": "Золото",
"atomicNumber": 79,
"atomicMass": 196.967,
"electronegativity": 2.54,
"category": "transition-metal",
"state": "solid",
"color": "#ffd123",
"description": "Extremely unreactive noble metal. Does not corrode or tarnish. Excellent conductor. Very rare.",
"descriptionRu": "Крайне инертный благородный металл. Не корродирует и не тускнеет. Отличный проводник. Очень редок."
},
{
"symbol": "Hg",
"name": "Mercury",
"nameRu": "Ртуть",
"atomicNumber": 80,
"atomicMass": 200.592,
"electronegativity": 2.00,
"category": "transition-metal",
"state": "liquid",
"color": "#b8b8d0",
"description": "Only metal that is liquid at room temperature. Extremely toxic — damages brain and kidneys. Handle with extreme care.",
"descriptionRu": "Единственный металл, жидкий при комнатной температуре. Крайне токсичен — поражает мозг и почки. Обращаться с предельной осторожностью."
}
]

326
src/data/reactions.json Normal file
View File

@@ -0,0 +1,326 @@
[
{
"id": "synth_nacl", "type": "synthesis",
"reactants": [{ "id": "Na", "count": 1 }, { "id": "Cl", "count": 1 }],
"products": [{ "id": "NaCl", "count": 1 }],
"energyChange": -40,
"description": "Sodium and chlorine combine in a vigorous exothermic reaction to form table salt.",
"descriptionRu": "Натрий и хлор соединяются в бурной экзотермической реакции, образуя поваренную соль.",
"difficulty": 1
},
{
"id": "synth_kcl", "type": "synthesis",
"reactants": [{ "id": "K", "count": 1 }, { "id": "Cl", "count": 1 }],
"products": [{ "id": "KCl", "count": 1 }],
"energyChange": -44,
"description": "Potassium bonds with chlorine to form potassium chloride — a valuable fertilizer salt.",
"descriptionRu": "Калий связывается с хлором, образуя хлорид калия — ценную удобрительную соль.",
"difficulty": 1
},
{
"id": "synth_fes", "type": "synthesis",
"reactants": [{ "id": "Fe", "count": 1 }, { "id": "S", "count": 1 }],
"products": [{ "id": "FeS", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -10,
"description": "Iron and sulfur combine when heated to form iron sulfide (pyrite) — fool's gold that sparks when struck.",
"descriptionRu": "Железо и сера при нагревании образуют сульфид железа (пирит) — золото дураков, искрящее при ударе.",
"difficulty": 2
},
{
"id": "synth_cus", "type": "synthesis",
"reactants": [{ "id": "Cu", "count": 1 }, { "id": "S", "count": 1 }],
"products": [{ "id": "CuS", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -5,
"description": "Copper reacts with sulfur when heated, forming dark copper sulfide.",
"descriptionRu": "Медь реагирует с серой при нагревании, образуя тёмный сульфид меди.",
"difficulty": 2
},
{
"id": "synth_zns", "type": "synthesis",
"reactants": [{ "id": "Zn", "count": 1 }, { "id": "S", "count": 1 }],
"products": [{ "id": "ZnS", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -20,
"description": "Zinc and sulfur combine exothermically to form zinc sulfide — a phosphorescent compound that glows in the dark.",
"descriptionRu": "Цинк и сера экзотермически образуют сульфид цинка — фосфоресцирующее соединение, светящееся в темноте.",
"difficulty": 2
},
{
"id": "synth_h2o", "type": "synthesis",
"reactants": [{ "id": "H", "count": 2 }, { "id": "O", "count": 1 }],
"products": [{ "id": "H2O", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -70,
"description": "Hydrogen combustion. Two parts hydrogen + one part oxygen ignite to produce water, releasing tremendous energy.",
"descriptionRu": "Горение водорода. Два объёма водорода + один объём кислорода воспламеняются, образуя воду и выделяя огромную энергию.",
"difficulty": 2
},
{
"id": "synth_hcl", "type": "synthesis",
"reactants": [{ "id": "H", "count": 1 }, { "id": "Cl", "count": 1 }],
"products": [{ "id": "HCl", "count": 1 }],
"energyChange": -9,
"description": "Hydrogen and chlorine combine to form hydrochloric acid — a strong acid that dissolves metals.",
"descriptionRu": "Водород и хлор образуют соляную кислоту — сильную кислоту, растворяющую металлы.",
"difficulty": 1
},
{
"id": "comb_co2", "type": "combustion",
"reactants": [{ "id": "C", "count": 1 }, { "id": "O", "count": 2 }],
"products": [{ "id": "CO2", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -40,
"description": "Carbon burns in excess oxygen to produce carbon dioxide. Complete combustion.",
"descriptionRu": "Углерод горит в избытке кислорода, образуя углекислый газ. Полное сгорание.",
"difficulty": 1
},
{
"id": "comb_co", "type": "combustion",
"reactants": [{ "id": "C", "count": 1 }, { "id": "O", "count": 1 }],
"products": [{ "id": "CO", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -11,
"description": "Carbon burns in limited oxygen → deadly carbon monoxide. Odorless killer.",
"descriptionRu": "Углерод горит при недостатке кислорода → смертельный угарный газ. Убийца без запаха.",
"difficulty": 2
},
{
"id": "comb_so2", "type": "combustion",
"reactants": [{ "id": "S", "count": 1 }, { "id": "O", "count": 2 }],
"products": [{ "id": "SO2", "count": 1 }],
"conditions": { "minTemp": 300 },
"energyChange": -30,
"description": "Sulfur burns with a blue flame, producing choking sulfur dioxide gas.",
"descriptionRu": "Сера горит синим пламенем, выделяя удушливый сернистый газ.",
"difficulty": 1
},
{
"id": "comb_mgo", "type": "combustion",
"reactants": [{ "id": "Mg", "count": 1 }, { "id": "O", "count": 1 }],
"products": [{ "id": "MgO", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -60,
"description": "Magnesium burns with an intensely bright white flame — blinding! Produces white magnesium oxide ash.",
"descriptionRu": "Магний горит ослепительно ярким белым пламенем! Образует белый пепел оксида магния.",
"difficulty": 1
},
{
"id": "comb_fe2o3", "type": "combustion",
"reactants": [{ "id": "Fe", "count": 2 }, { "id": "O", "count": 3 }],
"products": [{ "id": "Fe2O3", "count": 1 }],
"conditions": { "minTemp": 1000 },
"energyChange": -25,
"description": "Iron oxidation. At furnace temperatures, iron combines with oxygen to form iron oxide (rust).",
"descriptionRu": "Окисление железа. При температуре печи железо соединяется с кислородом, образуя оксид железа (ржавчину).",
"difficulty": 3
},
{
"id": "comb_cao", "type": "combustion",
"reactants": [{ "id": "Ca", "count": 1 }, { "id": "O", "count": 1 }],
"products": [{ "id": "CaO", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -63,
"description": "Calcium burns in oxygen to form quicklime — a powerful desiccant and heat source when mixed with water.",
"descriptionRu": "Кальций горит в кислороде, образуя негашёную известь — мощный осушитель и источник тепла при смешении с водой.",
"difficulty": 2
},
{
"id": "comb_zno", "type": "combustion",
"reactants": [{ "id": "Zn", "count": 1 }, { "id": "O", "count": 1 }],
"products": [{ "id": "ZnO", "count": 1 }],
"conditions": { "minTemp": 500 },
"energyChange": -35,
"description": "Zinc burns with a blue-green flame, producing zinc oxide — a healing white powder.",
"descriptionRu": "Цинк горит сине-зелёным пламенем, образуя оксид цинка — целебный белый порошок.",
"difficulty": 2
},
{
"id": "comb_sio2", "type": "combustion",
"reactants": [{ "id": "Si", "count": 1 }, { "id": "O", "count": 2 }],
"products": [{ "id": "SiO2", "count": 1 }],
"conditions": { "minTemp": 1000 },
"energyChange": -90,
"description": "Silicon combines with oxygen at extreme heat to form silica (quartz) — the basis of glass.",
"descriptionRu": "Кремний соединяется с кислородом при экстремальном нагреве, образуя кремнезём (кварц) — основу стекла.",
"difficulty": 4
},
{
"id": "comb_al2o3", "type": "combustion",
"reactants": [{ "id": "Al", "count": 2 }, { "id": "O", "count": 3 }],
"products": [{ "id": "Al2O3", "count": 1 }],
"conditions": { "minTemp": 1000 },
"energyChange": -84,
"description": "Aluminum combustion produces alumina (corundum) — nearly as hard as diamond.",
"descriptionRu": "Горение алюминия производит корунд — почти такой же твёрдый, как алмаз.",
"difficulty": 4
},
{
"id": "synth_ch4", "type": "synthesis",
"reactants": [{ "id": "C", "count": 1 }, { "id": "H", "count": 4 }],
"products": [{ "id": "CH4", "count": 1 }],
"conditions": { "minTemp": 500, "catalyst": "Fe" },
"energyChange": -8,
"description": "Carbon + hydrogen with iron catalyst → methane. Simplest hydrocarbon fuel.",
"descriptionRu": "Углерод + водород с железным катализатором → метан. Простейшее углеводородное топливо.",
"difficulty": 3
},
{
"id": "synth_naoh", "type": "synthesis",
"reactants": [{ "id": "Na", "count": 1 }, { "id": "O", "count": 1 }, { "id": "H", "count": 1 }],
"products": [{ "id": "NaOH", "count": 1 }],
"energyChange": -45,
"description": "Direct synthesis of sodium hydroxide (lye). Extremely caustic — handle carefully!",
"descriptionRu": "Прямой синтез гидроксида натрия (щёлочи). Крайне едкий — обращаться осторожно!",
"difficulty": 2
},
{
"id": "synth_koh", "type": "synthesis",
"reactants": [{ "id": "K", "count": 1 }, { "id": "O", "count": 1 }, { "id": "H", "count": 1 }],
"products": [{ "id": "KOH", "count": 1 }],
"energyChange": -49,
"description": "Direct synthesis of potassium hydroxide. Even more reactive than NaOH.",
"descriptionRu": "Прямой синтез гидроксида калия. Ещё активнее, чем NaOH.",
"difficulty": 2
},
{
"id": "synth_caco3", "type": "synthesis",
"reactants": [{ "id": "Ca", "count": 1 }, { "id": "C", "count": 1 }, { "id": "O", "count": 3 }],
"products": [{ "id": "CaCO3", "count": 1 }],
"energyChange": -12,
"description": "Calcium, carbon, and oxygen form calcium carbonate — the building block of limestone and shells.",
"descriptionRu": "Кальций, углерод и кислород образуют карбонат кальция — строительный блок известняка и ракушек.",
"difficulty": 3
},
{
"id": "synth_nahco3", "type": "synthesis",
"reactants": [{ "id": "Na", "count": 1 }, { "id": "H", "count": 1 }, { "id": "C", "count": 1 }, { "id": "O", "count": 3 }],
"products": [{ "id": "NaHCO3", "count": 1 }],
"energyChange": -10,
"description": "Synthesis of sodium bicarbonate (baking soda). Versatile acid neutralizer.",
"descriptionRu": "Синтез гидрокарбоната натрия (пищевая сода). Универсальный нейтрализатор кислот.",
"difficulty": 3
},
{
"id": "synth_kno3", "type": "synthesis",
"reactants": [{ "id": "K", "count": 1 }, { "id": "N", "count": 1 }, { "id": "O", "count": 3 }],
"products": [{ "id": "KNO3", "count": 1 }],
"energyChange": -5,
"description": "Potassium nitrate (saltpeter). A powerful oxidizer — one third of gunpowder.",
"descriptionRu": "Нитрат калия (селитра). Мощный окислитель — треть состава пороха.",
"difficulty": 3
},
{
"id": "synth_caoh2", "type": "synthesis",
"reactants": [{ "id": "Ca", "count": 1 }, { "id": "O", "count": 2 }, { "id": "H", "count": 2 }],
"products": [{ "id": "CaOH2", "count": 1 }],
"energyChange": -15,
"description": "Calcium hydroxide (slaked lime). Essential for mortar and water purification.",
"descriptionRu": "Гидроксид кальция (гашёная известь). Необходим для строительного раствора и очистки воды.",
"difficulty": 3
},
{
"id": "repl_na_h2o", "type": "single-replacement",
"reactants": [{ "id": "Na", "count": 1 }, { "id": "H2O", "count": 1 }],
"products": [{ "id": "NaOH", "count": 1 }, { "id": "H", "count": 1 }],
"energyChange": -80,
"description": "VIOLENT! Sodium reacts explosively with water → lye + hydrogen gas + fire. Never drop sodium in water!",
"descriptionRu": "ОПАСНО! Натрий взрывоподобно реагирует с водой → щёлочь + водород + пламя. Никогда не бросайте натрий в воду!",
"difficulty": 1
},
{
"id": "repl_k_h2o", "type": "single-replacement",
"reactants": [{ "id": "K", "count": 1 }, { "id": "H2O", "count": 1 }],
"products": [{ "id": "KOH", "count": 1 }, { "id": "H", "count": 1 }],
"energyChange": -95,
"description": "EXPLOSIVE! Potassium reacts even more violently than sodium with water — purple flame, detonation!",
"descriptionRu": "ВЗРЫВ! Калий реагирует с водой ещё бурнее натрия — фиолетовое пламя, детонация!",
"difficulty": 1
},
{
"id": "repl_cao_h2o", "type": "single-replacement",
"reactants": [{ "id": "CaO", "count": 1 }, { "id": "H2O", "count": 1 }],
"products": [{ "id": "CaOH2", "count": 1 }],
"energyChange": -65,
"description": "Quicklime + water → slaked lime + intense heat. Can boil water! Used in ancient warfare.",
"descriptionRu": "Негашёная известь + вода → гашёная известь + сильный нагрев. Может вскипятить воду! Использовалось в древних войнах.",
"difficulty": 1
},
{
"id": "ab_naoh_hcl", "type": "acid-base",
"reactants": [{ "id": "NaOH", "count": 1 }, { "id": "HCl", "count": 1 }],
"products": [{ "id": "NaCl", "count": 1 }, { "id": "H2O", "count": 1 }],
"energyChange": -57,
"description": "Classic neutralization: acid + base → salt + water. NaOH + HCl → NaCl + H₂O.",
"descriptionRu": "Классическая нейтрализация: кислота + основание → соль + вода. NaOH + HCl → NaCl + H₂O.",
"difficulty": 2
},
{
"id": "ab_koh_hcl", "type": "acid-base",
"reactants": [{ "id": "KOH", "count": 1 }, { "id": "HCl", "count": 1 }],
"products": [{ "id": "KCl", "count": 1 }, { "id": "H2O", "count": 1 }],
"energyChange": -57,
"description": "Neutralization: potassium hydroxide + hydrochloric acid → potassium chloride + water.",
"descriptionRu": "Нейтрализация: гидроксид калия + соляная кислота → хлорид калия + вода.",
"difficulty": 2
},
{
"id": "ab_nahco3_hcl", "type": "acid-base",
"reactants": [{ "id": "NaHCO3", "count": 1 }, { "id": "HCl", "count": 1 }],
"products": [{ "id": "NaCl", "count": 1 }, { "id": "H2O", "count": 1 }, { "id": "CO2", "count": 1 }],
"energyChange": -12,
"description": "Baking soda fizzes violently with acid! Produces salt, water, and carbon dioxide gas. Classic volcano reaction.",
"descriptionRu": "Сода бурно шипит при контакте с кислотой! Образуется соль, вода и углекислый газ. Классическая реакция «вулкан».",
"difficulty": 1
},
{
"id": "synth_gunpowder", "type": "synthesis",
"reactants": [{ "id": "KNO3", "count": 1 }, { "id": "S", "count": 1 }, { "id": "C", "count": 1 }],
"products": [{ "id": "GUNPOWDER", "count": 1 }],
"energyChange": 0,
"description": "The legendary mixture: saltpeter + sulfur + charcoal = black gunpowder. Handle with extreme care.",
"descriptionRu": "Легендарная смесь: селитра + сера + уголь = чёрный порох. Обращаться с предельной осторожностью.",
"difficulty": 4
},
{
"id": "redox_thermite", "type": "redox",
"reactants": [{ "id": "Fe2O3", "count": 1 }, { "id": "Al", "count": 2 }],
"products": [{ "id": "Fe", "count": 2 }, { "id": "Al2O3", "count": 1 }],
"conditions": { "minTemp": 1000 },
"energyChange": -100,
"description": "THERMITE! Iron oxide + aluminum → molten iron + alumina. Temperature exceeds 2500°C. Melts through anything.",
"descriptionRu": "ТЕРМИТ! Оксид железа + алюминий → расплавленное железо + корунд. Температура превышает 2500°C. Прожигает всё.",
"difficulty": 5
},
{
"id": "synth_ethanol", "type": "synthesis",
"reactants": [{ "id": "C", "count": 2 }, { "id": "H", "count": 6 }, { "id": "O", "count": 1 }],
"products": [{ "id": "C2H5OH", "count": 1 }],
"conditions": { "catalyst": "Cu" },
"energyChange": -3,
"description": "Ethanol synthesis requires a copper catalyst. The original chemistry of civilization — fermentation.",
"descriptionRu": "Синтез этанола требует медного катализатора. Изначальная химия цивилизации — ферментация.",
"difficulty": 4
},
{
"id": "decomp_h2o", "type": "decomposition",
"reactants": [{ "id": "H2O", "count": 1 }],
"products": [{ "id": "H", "count": 2 }, { "id": "O", "count": 1 }],
"conditions": { "requiresEnergy": true },
"energyChange": 70,
"description": "Electrolysis: electric current splits water into hydrogen and oxygen. Endothermic — needs energy input.",
"descriptionRu": "Электролиз: электрический ток разлагает воду на водород и кислород. Эндотермическая — требует энергии.",
"difficulty": 3
},
{
"id": "decomp_caco3", "type": "decomposition",
"reactants": [{ "id": "CaCO3", "count": 1 }],
"products": [{ "id": "CaO", "count": 1 }, { "id": "CO2", "count": 1 }],
"conditions": { "minTemp": 1000 },
"energyChange": 18,
"description": "Lime burning: heating limestone drives off CO₂, leaving quicklime. Ancient building technology.",
"descriptionRu": "Обжиг извести: нагрев известняка выгоняет CO₂, оставляя негашёную известь. Древняя строительная технология.",
"difficulty": 3
}
]

347
tests/chemistry.test.ts Normal file
View File

@@ -0,0 +1,347 @@
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);
}
}
});
});