diff --git a/.playwright-mcp/console-2026-02-12T09-06-28-438Z.log b/.playwright-mcp/console-2026-02-12T09-06-28-438Z.log new file mode 100644 index 0000000..ab04aa5 --- /dev/null +++ b/.playwright-mcp/console-2026-02-12T09-06-28-438Z.log @@ -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 diff --git a/PROGRESS.md b/PROGRESS.md index 3dc67e0..f94e4bd 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -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 1 — Chemistry Engine +## Up Next: Phase 2 — ECS 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 | diff --git a/boot-screen.png b/boot-screen.png new file mode 100644 index 0000000..c5fa8f5 Binary files /dev/null and b/boot-screen.png differ diff --git a/src/chemistry/compounds.ts b/src/chemistry/compounds.ts new file mode 100644 index 0000000..1560825 --- /dev/null +++ b/src/chemistry/compounds.ts @@ -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(); + +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; diff --git a/src/chemistry/elements.ts b/src/chemistry/elements.ts new file mode 100644 index 0000000..8e890fa --- /dev/null +++ b/src/chemistry/elements.ts @@ -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(); +const byNumber = new Map(); + +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; diff --git a/src/chemistry/engine.ts b/src/chemistry/engine.ts new file mode 100644 index 0000000..7bcfebf --- /dev/null +++ b/src/chemistry/engine.ts @@ -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(); + +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, +): { 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(a: Set, b: Set): 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 | 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, + ): 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; diff --git a/src/chemistry/types.ts b/src/chemistry/types.ts new file mode 100644 index 0000000..3c098af --- /dev/null +++ b/src/chemistry/types.ts @@ -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; +} diff --git a/src/data/compounds.json b/src/data/compounds.json new file mode 100644 index 0000000..fa4c0f5 --- /dev/null +++ b/src/data/compounds.json @@ -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"] + } +] diff --git a/src/data/elements.json b/src/data/elements.json new file mode 100644 index 0000000..b11ddd0 --- /dev/null +++ b/src/data/elements.json @@ -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": "Единственный металл, жидкий при комнатной температуре. Крайне токсичен — поражает мозг и почки. Обращаться с предельной осторожностью." + } +] diff --git a/src/data/reactions.json b/src/data/reactions.json new file mode 100644 index 0000000..79eaa0b --- /dev/null +++ b/src/data/reactions.json @@ -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 + } +] diff --git a/tests/chemistry.test.ts b/tests/chemistry.test.ts new file mode 100644 index 0000000..e9f6066 --- /dev/null +++ b/tests/chemistry.test.ts @@ -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); + } + } + }); +});