Show HN:Wyrm – 通过触控求解代数方程,基于开源可靠性引擎构建
Show HN: Wyrm – Solve algebra by touch, built on an open-source soundness engine

原始链接: https://github.com/dicroce/wyrm_math

**wyrm-math** 是一个高精度、无依赖的 TypeScript 引擎,专为构建交互式、基于手势的代数界面而设计。该引擎通过使用 `bigint` 的精确有理数运算取代浮点数算术,从构造层面而非验证层面保证了结构的完整性和数学的严谨性。 其核心理念是**条件稳健性**:将引入限制(如 $b \neq 0$)或无关解的操作视为一等公民“假设”(Assumptions),并使其随方程持续存在。由于该引擎基于状态且不可变,每一次转换都是追加式的推导,从而便于实现分支、撤销和复杂的分类讨论。 主要技术特性包括: * **稳定的 AST ID**:在方程转换过程中支持无缝的 ID 键值 UI 动画。 * **内置规则**:包含约 25 条针对线性、二次及分式方程的验证规则。 * **布局引擎**:生成与几何无关的定位数据,使开发者无需 DOM 访问即可在任何平台(Node、Web、Native)上渲染方程。 * **严谨测试**:项目优先采用基于属性的测试,以确保每一条重写规则都能保持解集不变。 无论是在 iOS、Android 还是 Web 上构建应用,wyrm-math 都能为现代教育软件提供坚实且以数学为核心的底层支持。

开发者 dicroce 推出了名为 **Wyrm** 的移动应用程序,旨在通过直观的基于手势的交互来教授代数。受热门游戏《DragonBox》的启发,Wyrm 允许用户通过物理拖拽、抵消和分配项来求解代数方程。 与其他教育类游戏不同,Wyrm 允许用户输入自定义题目,并旨在实现从基础代数到微积分的全覆盖。该项目建立在一个功能强大的开源数学引擎(MIT 许可)`wyrm_math` 之上,开发者鼓励他人进行查阅和在此基础上进行开发。 Wyrm 目前在 iOS App Store 和 Google Play 上以 4.99 美元的价格出售。开发者选择了直接的商业模式,不包含任何订阅、广告或数据追踪。官网提供了一个有限的浏览器演示版本,供希望在购买前测试界面的用户使用。目前开发者正在寻求反馈,特别欢迎来自计算机代数系统 (CAS) 或证明助手设计领域的专家提供意见。
相关文章

原文

An exact, conditionally-sound symbolic algebra engine for building manipulative math interfaces — the kind where users solve equations by dragging terms across the equals sign, tapping a power to expand it, or pulling a shared factor out of two terms.

The core invariant: legal moves are possible, illegal moves are impossible. Equations are never validated — they are only ever transformed by rewrite rules, so every reachable state is sound by construction. And soundness is conditional: moves that are only valid under a condition (dividing by b requires b ≠ 0) or that can introduce extraneous solutions (multiplying both sides, squaring) are not forbidden — their conditions become first-class, visible Assumptions that travel with the equation.

Pure TypeScript, zero dependencies, zero DOM — runs in Node, browsers, workers, native webviews, anywhere.

wyrm-math is the engine behind Wyrm Math, a gesture-based algebra app for iOS and Android — try the in-browser demo or get the app. The engine is MIT; the app is how the project sustains itself.

import {
  parseEquation, Derivation,
  enumerateMoves, ruleById, layoutNode, exprToString,
} from "wyrm-math";

const d = new Derivation(parseEquation("2x + 3 = 11"));

// What can the user legally do right now?
const moves = enumerateMoves(d.current);

// Drag the 3 across the equals sign (the UI picks a Move; the engine
// guarantees it is legal — enumeration is precondition-checked):
const move = moves.find((m) => m.ruleId === "move-term-across")!;
d.apply(ruleById(move.ruleId), move.location, move.params);

console.log(exprToString(d.current.equation)); // 2x = 11 + -3

// Render it however you like: layoutNode gives positioned, id-keyed boxes
// and glyphs from static metric tables (no font measurement needed).
const layout = layoutNode(d.current.equation);

The public API is src/index.ts, organized into ten documented groups — it reads as a table of contents:

Group What it gives you
Expression trees Immutable AST with stable node ids. N-ary Sum/Product; no subtraction or division nodes (a − b is Sum(a, Neg(b)); division is a Fraction with numerator/denominator lists). Smart constructors maintain the structural invariants.
Exact arithmetic Rational over bigint. No floating point anywhere — √2 is an undefined point, not 1.4142.
Evaluation truthValue(equation, env) decides any relation (= < ≤ > ≥) at a sample point, exactly, or returns undefined where a side is undefined.
Parsing & printing parseEquation("2x + 3 = 11")exprToString — round-trip property-tested. Implicit multiplication, fractions, powers, radicals; decimals rejected (the engine is exact).
Judgments & assumptions The unit of state is { assumptions, equation }. Restrictions (moves that may LOSE solutions: b ≠ 0), Extensions (moves that may GAIN them: carry the original equation as an obligation, settled by checkSolution), Pinned (user what-ifs). Discharged assumptions are recorded, never deleted.
Rules & derivations Rule.apply is the only way an equation changes. The derivation log is an append-only tree: undo moves a pointer, abandoned branches stay live, case splits and disjunctions fork into live siblings.
Built-in rules ~25 rules covering linear equations, like terms, distribution, fractions, exponent laws, inequalities (sign-aware, relation-flipping), and quadratics (x² = 9 branches to x = ±3; zero-product). Every rule ships with a property test that it respects the solution set under its assumptions.
Move enumeration enumerateMoves(judgment) returns every legal affordance with gesture anchors (handle, dropTarget). Sound for all rules, complete for the finite ones. Pin x = 0 and every divide-by-x affordance disappears automatically.
Layout geometry layoutNode maps trees to positioned, id-keyed boxes and glyphs (fraction stacking, superscripts, radicals) from static metric tables. hitTest is a geometry query. Subtree geometry is context-independent up to translation+scale — which is what makes id-keyed animation possible.
Rule-authoring toolkit Id-preserving rebuilds, the invariant-repairing splice, diff bookkeeping, and assumption-lifecycle queries for writing new rules.

ARCHITECTURE.md explains the invariants and contracts in depth.

  • Exactness. All arithmetic is bigint rationals. Points where an expression is undefined (division by zero, irrational roots) are treated as undefined, never approximated. The engine-wide soundness contract is truth-where-both-defined.
  • Stable ids. Every node has an id; operations preserve the ids of untouched subtrees. This is the currency of hit testing and animation: a renderer can match nodes across a rewrite and move them rigidly.
  • Conditional soundness. For ordinary and Restriction-emitting rules, property tests rejection-sample substitutions to those satisfying the result judgment's assumptions and assert truth preservation. For Extension-emitting rules the check weakens to one direction (solutions are never lost), with checkSolution covering the gain obligation.
  • Disjunction. Branching rules return several outcomes whose solution sets union to the original's (x² = 9x = 3 or x = −3); the derivation tree holds all arms as live, navigable states.
pnpm install
pnpm test        # vitest + fast-check (property tests are the soul of this project)
pnpm typecheck
pnpm build       # emits dist/ (ESM + d.ts)

The engine must stay DOM-free: tsconfig.json has no DOM lib and test/boundary.test.ts scans the sources for browser globals.

MIT

联系我们 contact @ memedata.com