Sydney Train Game; Get 10
Here lies an embedded iframe of my typescript implementation of the Sydney Trains "Make 10" game.
We use a recursive dfs on all parenthesised permutations. See the README file for more details.
rules
Given a series of 4 numbers (the train carriage identifier), we mentally tasked with constructing the number 10 using mathematical operations.
Say our carriage number was: \[\require{bbox}\bbox[lightblue,5px,border:2px solid red]{\color{#800000}{ 6325 }}\]
Then one valid configuration would be \(6-3+2+5\) which equals 10.
iframe
implementation (algorithm)
note that we are only using binary operators (plus, minus, divide, subtract and power).
skipping factorials and roots simplifies our implementation and forgoes the building of a proper expression parser / ast ⊕
export type Op = '+' | '-' | '*' | '/' | '^';
type Node = {
val: number;
expr: string;
}
// epsilon for division
const EPS = 1e-9;
// in-order solver: keep digits in order, vary parethesis and ops
export function solveToTen(digits: number[], target=10, ops: ReadonlyArray<Op> = ['+','-','*','/']): string[] {
const start: Node[] = digits.map((d)=> ({val: d, expr: `${d}` })); // populate from func args
const allowed = new Set(ops);
const seen = new Set <string>();
function safePow(a: number, b: number): number | null {
// keep it real and bounded
if (!Number.isFinite(a) || !Number.isFinite(b)) return null; // float
if (Math.abs(b) > 9) return null; // too large
if (a < 0 && Math.abs(b - Math.trunc(b)) > EPS) return null; // too small
const out = a ** b; // ^ is xor in js / ts; duh
if (!Number.isFinite(out) || Math.abs(out) > 1e9) return null;
return out;
}
function combine(a: Node, b: Node): Node[] {
const out: Node[] = [];
if (allowed.has('+')) out.push( { val: a.val + b.val, expr: `(${a.expr}+${b.expr})` });
if (allowed.has('-')) out.push( { val: a.val - b.val, expr: `(${a.expr}-${b.expr})` });
if (allowed.has('*')) out.push( { val: a.val * b.val, expr: `(${a.expr}*${b.expr})` });
if (allowed.has('/')) if (Math.abs(b.val)>EPS) out.push({val:a.val/b.val, expr:`(${a.expr}/${b.expr})`});
if (allowed.has('^')) {
const p = safePow(a.val, b.val);
if (p != null) out.push({ val: p, expr: `(${a.expr}^${b.expr})` }); // note internal data structure is ^ display.
}
return out;
}
function rec(nodes: Node[]) {
if (nodes.length === 1) {
if (Math.abs(nodes[0].val - target) < EPS) {
// drop outermost parethesis:
seen.add(nodes[0].expr.replace(/^\((.*)\)$/, '$1'));
/**
* / start
* ^ anchors to beginning of string
* \( matches the ( literal
* (.*) capturing group; matches any char >=0 times.
* \) matches ) literal
* $ anchors to end of string
* / end of exp.
* $1 first and only capturing group.
**/
}
return;
}
// combine any adjacent pair. enumerates all in-order parenthesisations.
for (let i = 0; i < nodes.length - 1; i++) { // tries every adjacent pair loop
const a = nodes[i], b = nodes[i + 1]; // sliding window adjacent pair combine
for (const nxt of combine(a, b)) { // pushes allowable ops into Node[]. checks ops loop
const nextNodes = nodes.slice(0, i).concat(nxt, nodes.slice(i+2));
rec(nextNodes);
}
}
}
rec(start);
return Array.from(seen).sort();
}