The Countku engine converts English word phrases into executable JavaScript math expressions, with automatic 5-7-5 haiku structure validation. It powers the Countku game mode in Sakura Count Ninja.
The rules tables are the language. The JavaScript adds only the mechanics that cannot live in a row: longest-match tokenization, precedence, numeric state, and line boundaries.
wordGroups assigns every complete legal word exactly one syllable count.
Phrase recognition never supplies a second count. This is important:
cube root of may be one mathematical token, but the haiku still sees three
indivisible English words.
HaikuValidator reads those whole words into 5, 7, and 5. A word that would
cross a boundary is rejected at that word. Explicit / separators are accepted
for copying examples, but must describe the same exact 5-7-5.
phraseRows contains fixed phrases such as roots, functions, logarithms, and
legal filler. Longest match wins, so:
under the influence of division from is the passive flipped division
operator;under the influence of by itself is removable filler;log base begins a base-first logarithm;cube root of and ordinal root of share the same root AST.Table 4 is generated from:
the;That Cartesian construction covers the table without duplicating nearly identical branches.
CountkuParser is a Pratt parser:
| Precedence | Forms |
|---|---|
| Prefix | roots, logs, trig, inverse, half/double, add/subtract |
| Postfix | squared, cubed, halved, doubled, tripled, quadrupled |
| 30 | powers (right associative) |
| 20 | multiplication, division, modulo, traditional adjacency |
| 10 | addition and subtraction |
Function arguments close after their direct grammatical object. Thus
sine of zero minus three is sin(0) - 3, while nested functions remain
nested.
After exact filler phrases are removed, adjacent complete mathematical objects use traditional implicit multiplication. For example:
the cube root of eight / under the influence of / eighth power of six
is a valid 5-7-5 and becomes:
\sqrt[3]{8}\,6^8
A valid parse produces a small AST (number, constant, unary, binary,
or base-log). Three pure walkers consume it:
evaluateAst gives the game result without eval;renderJs gives a readable executable expression;renderLatex gives the stable textbook representation.The live console shows all three views. They cannot disagree because none of them reparses the sentence.
point, only zero forms, digits one through nine, or ten are legal.$AndMatrix records whether each three-digit group uses its and.zero/zed/oh), multiplication (times/multiplied by), Euler,
and $AndMatrix choices become run locks only after a successful submission.
Live typing never mutates them.The sections below document the pre-v8 implementation and remain as design
history. Names such as COUNTKU_DB, COUNTKU_MULTI, and convertTokens() no
longer describe the live engine.
Single-word entries indexed by lowercase word. Each entry has:
| Field | Type | Description |
|---|---|---|
syl |
number | Syllable count for haiku validation |
type |
string | number, operator, function, constant, helper, decimal, multiplier, ordinal, noise, special |
val |
string | Math expression value (for numbers, operators, functions) |
role |
string | Sub-type for special entries (e.g., decimal_zero for “oh”) |
pAction |
boolean | $P_Action flag: sets prepMode = 'action' |
pPassiv |
boolean | $P_Passiv flag: sets prepMode = 'passiv' |
nDiv/nMul/nAdd/nSub |
boolean | Noun operator flags (division, multiplication, addition, subtraction) |
closeFunc |
boolean | Closes open function parens when encountered |
andRole |
boolean | Tracks “and” usage for $AndMatrix state |
sylOf |
number | Extra syllables when followed by “of” |
Key Entries:
| Word | Type | Value | Syl |
|---|---|---|---|
| zero | number | 0 | 2 |
| zed | number | 0 | 1 |
| oh | special | 0 (decimal only) | 1 |
| one-nineteen | number | 1-19 | 1-3 |
| twenty-ninety | number | 20-90 | 2-3 |
| hundred | multiplier | *100 | 2 |
| thousand | multiplier | *1000 | 2 |
| million | multiplier | *1000000 | 2 |
| point | decimal | . | 1 |
| plus | operator | + | 1 |
| minus | operator | - | 2 |
| times | operator | * | 1 |
| divided | operator | / (prefix) | 3 |
| by | helper | - | 1 |
| over | operator | / | 2 |
| modulo | operator | % | 3 |
| squared | operator | **2 | 1 |
| cubed | operator | **3 | 1 |
| halved | operator | /2 | 1 |
| doubled | operator | *2 | 2 |
| tripled | operator | *3 | 2 |
| quadrupled | operator | *4 | 3 |
| pi | constant | Math.PI | 1 |
| e | constant | Math.E | 1 |
| log | function | Math.log( | 1 |
| root | function | Math.sqrt( | 1 |
| sine | function | Math.sin( | 1 |
| cosine | function | Math.cos( | 2 |
| tan | function | Math.tan( | 1 |
| sqrt | function | Math.sqrt( | 1 |
| cbrt | function | Math.cbrt( | 1 |
| abs | function | Math.abs( | 1 |
| the | helper | noise | 1 |
| of | helper | noise | 1 |
| and | helper | digit linker | 1 |
| to | helper | power trigger (v6) | 1 |
| with | helper | $P_Action | 1 |
| using | helper | $P_Action | 2 |
| by | helper | $P_Action | 1 |
| under | helper | $P_Passiv | 2 |
| undergoing | helper | $P_Passiv | 4 |
| addition | helper | $N_Add (+) | 3 |
| subtraction | helper | $N_Sub (-) | 3 |
| multiplication | helper | $N_Mul (*) | 5 |
| division | helper | $N_Div (/) | 3 |
Longest-first matching patterns for phrases that span multiple words:
| Words | Math | Syl | Op | Notes |
|---|---|---|---|---|
| to the power of | ** | 5 | d17 | Power trigger (v6 deferred) |
| to the power | ** | 4 | d17 | Short power trigger |
| square root of | Math.sqrt( | 4 | d06 | |
| cube root of | Math.cbrt( | 4 | d07 | |
| nth root of | Math.pow($,1/N) | 4 | d08 | Ordinal placeholder |
| natural log of | Math.log( | 4 | d16 | |
| log base N of | Math.log($)/Math.log(N) | 5 | d18 | With base parameter |
| inverse of | 1/$ | 4 | d09 | |
| sine of | Math.sin( | 2 | d10 | |
| cosine of | Math.cos( | 3 | d11 | |
| tangent of | Math.tan( | 3 | d12 | |
| secant of | 1/Math.cos( | 3 | d13 | |
| cosecant of | 1/Math.sin( | 4 | d14 | |
| cotangent of | 1/Math.tan( | 4 | d15 | |
| arcsine of | Math.asin( | 3 | ||
| arccosine of | Math.acos( | 4 | ||
| arctangent of | Math.atan( | 4 | ||
| half of | (1/2)* | 2 | d37 | Sets pendingHalf |
| double | 2* | 2 | d38 | Sets pendingDouble |
| twice | 2* | 1 | ||
| the influence of | - | 5 | noise | $P00_Noise |
| the total of | - | 4 | noise | $P00_Noise |
| the effect of | - | 4 | noise | $P00_Noise |
| an effect of | - | 4 | noise | $P00_Noise |
| a total of | - | 4 | noise | $P00_Noise |
| under the influence of | - | 7 | noise | $P00_Noise |
| subtraction from | FLIP_SUB | 4 | d41 | B - A flip |
| multiplied by | * | 5 | ||
| divided by | / | 4 | ||
| euler’s number | Math.E | 5 |
| Word | Value | Syl |
|---|---|---|
| first | 1 | 1 |
| second | 2 | 2 |
| third | 3 | 1 |
| fourth | 4 | 1 |
| fifth | 5 | 1 |
| sixth | 6 | 1 |
| seventh | 7 | 2 |
| eighth | 8 | 1 |
| ninth | 9 | 1 |
| tenth | 10 | 1 |
| eleventh | 11 | 3 |
| twelfth | 12 | 1 |
| nth | N | 1 |
Composing ordinals that combine with following single-digit ordinals:
| Word | Base Value |
|---|---|
| twentieth | 20 |
| thirtieth | 30 |
| fortieth | 40 |
| fiftieth | 50 |
| sixtieth | 60 |
| seventieth | 70 |
| eightieth | 80 |
| ninetieth | 90 |
Usage: “sixty eighth” → 60 + 8 = 68
Input: "the square root of nine plus two"
↓
[Clean] Strip punctuation, commas, trailing .!?
↓
[Multi-word Match] Longest-first pattern matching
↓
[Token Stream] [{type:'multi', pattern:d06, words:['the','square','root','of']},
{type:'single', word:'nine'},
{type:'single', word:'plus'},
{type:'single', word:'two'}]
↓
[convertTokens] Process tokens → JS math expression
↓
Output: "Math.sqrt(9)+2"
Per-token state:
| Variable | Purpose |
|———-|———|
| parts[] | Accumulated expression fragments |
| openParens | Count of unclosed parentheses |
| lastWasNumber | Previous token was a number |
| prepMode | null / ‘action’ / ‘passiv’ |
| expectingFuncClose | Function opened, waiting for arg |
| inDecimal | Inside decimal (after “point”) |
| pendingHalf/Double | d37/d38 prefix operators |
Deferred emission state (v6):
| Variable | Purpose |
|———-|———|
| powerMode | null / ‘to’ / ‘ordinal’ |
| powerDegree | Stored degree for ordinal power |
| powerBaseExpr | Base expression string |
| powerWrap | ‘full’ (entire expr) / ‘lastTerm’ |
| prefixParts | Parts before last term (for lastTerm wrap) |
| rootMode | Boolean: in root mode |
| rootDegree | Root degree string |
| toReverso | Boolean: 1/N reversal |
| toOrdinalDegree | Captured ordinal degree |
| Flag | Trigger Words | Scope | Example Input | Output |
|---|---|---|---|---|
| $P_Action | using, with, by | Entire preceding expression | five plus three using the power of two |
(5+3)**2 |
| $P_Passiv | undergoing, under | Only last term | five plus three under the power of two |
5+(3)**2 |
Implementation:
$P_Action: powerBaseExpr = parts.join('') (everything)$P_Passiv: splitLastTerm(parts) splits at last top-level operatorSplits parts array at the last top-level operator:
// Input parts: ['5', '+', '3', '*', '2']
// Depth tracking: + is at depth 0, * is at depth 0
// Last top-level op: '*'
// Result: { before: ['5', '+', '3', '*'], term: '2' }
// Input parts: ['Math.sin(', '1', ')', '+', '5']
// Depth: Math.sin( → depth 1, ) → depth 0, + at depth 0
// Result: { before: ['Math.sin(', '1', ')', '+'], term: '5' }
Sums consecutive numeric tokens:
// Input: ['1000', '800', '58', '+', '3']
// Output: ['1858', '+', '3']
//
// Input: ['10', '5', '*', '2']
// Output: ['15', '*', '2']
| Input | Expression | Result |
|---|---|---|
| one plus two | 1+2 | 3 |
| five minus three | 5-3 | 2 |
| four times six | 4*6 | 24 |
| eight divided by two | 8/2 | 4 |
| ten over five | 10/5 | 2 |
| Input | Expression | Notes |
|---|---|---|
| twenty one | 21 | Tens + ones |
| one hundred | 100 | Base + multiplier |
| one hundred twenty three | 123 | Hundreds + tens + ones |
| three thousand four hundred fifty six | 3456 | sumDigitGroups |
| one point five | 1.5 | Decimal |
| one point oh five | 1.05 | Oh as decimal zero |
| Input | Expression | Result |
|---|---|---|
| the square root of sixteen | Math.sqrt(16) | 4 |
| the cube root of twenty seven | Math.cbrt(27) | 3 |
| the sine of zero | Math.sin(0) | 0 |
| the natural log of e | Math.log(Math.E) | 1 |
| the absolute value of minus five | Math.abs(-5) | 5 |
| Input | Expression | Notes |
|---|---|---|
| two to the power of three | (2)**3 | 8 |
| five to the sixtieth | (5)**60 | Deferred |
| ten to the fourth | (10)**4 | Ordinal degree |
| three squared | 3**2 | Postfix |
| four cubed | 4**3 | Postfix |
| Input | Expression | Notes |
|---|---|---|
| the fourth root of sixteen | (16)**(1/4) | 2 |
| a fiftieth of five | (5)**(1/50) | “of” trigger |
| the tessaric root of one hundred | (100)**(1/4) | Composed ordinal |
| the four hundred sixty eighth root of ten | (10)**(1/468) | Multi-word ordinal |
| Input | Expression | Mode |
|---|---|---|
| five plus three using the power of two | (5+3)**2 | $P_Action: full expr |
| five plus three under the power of two | 5+(3)**2 | $P_Passiv: last term |
| Input | Expression | Result |
|---|---|---|
| half of ten | (1/2)*10 | 5 |
| double five | 2*5 | 10 |
| twice three | 2*3 | 6 |
| ten halved | 10/2 | 5 |
| six doubled | 6*2 | 12 |
Automatic 5-7-5 line detection without explicit delimiters:
Current line syllables + next word syllables > target?
Yes → Start new line
No → Add to current line
Example: “the sine of zero plus the log of one and two”
| Word | Syl | Line Accum | Action |
|---|---|---|---|
| the | 1 | 1 | Line 1 |
| sine | 1 | 2 | Line 1 |
| of | 1 | 3 | Line 1 |
| zero | 2 | 5 | Line 1 (full) |
| plus | 1 | 1 | Line 2 |
| the | 1 | 2 | Line 2 |
| log | 1 | 3 | Line 2 |
| of | 1 | 4 | Line 2 |
| one | 1 | 5 | Line 2 |
| and | 1 | 6 | Line 2 |
| two | 1 | 7 | Line 2 (full) |
Words that cannot be split across line boundaries:
minus, multiplied, divided, seventeen, seventy, eleven,
hundred, thousand, million, billion, logarithm, absolute,
tangent, cosine, cotangent, secant, cosecant, number, power,
subtracted, subtraction, influence, undergoing, quadrupling,
addition, product, modulo, quadrupled, tripled, euler's,
halving, doubling, tripling, total, effect
Users must pick ONE variant per category and stick with it:
| Category | Options | Notes |
|---|---|---|
| Zero | zero (2), zed (1), oh (1) | Pick one, can’t mix |
| Multiply | times (1), multiplied by (5) | Pick one, can’t mix |
| Euler | e (1), euler’s number (5) | Pick one, can’t mix |
class CountkuConverter {
convertTokens(tokens) → { expr, parts } // Main conversion (v6 deferred)
convertPhrase(phrase) → expr|null // Tokenize + convert
countSyllables(phrase) → number // Syllable count
tokenize(text) → tokens[] // Multi-word + single-word
splitLastTerm(parts) → { before, term } // v6: $P_Passiv scope
sumDigitGroups(parts) → parts[] // v6: digit summing
resetVariants() // Reset variant tracker
checkVariant(word) → error|null // Variant consistency
getSyllables(word) → number|null // Syllable lookup
}
class HaikuValidator {
validate(phrase) → { isHaiku, error, lines[] }
}
| Mode | Base | Input Type | Validation |
|---|---|---|---|
| Normal | 10 | JS math expression | Expression evaluation |
| Hard | 2 | JS math expression | Expression evaluation, result shown in binary |
| WTF | 16 | JS math expression | Expression evaluation, result shown in hex |
| Countku | 10 | English words (haiku) | Haiku 5-7-5 + word-to-math conversion |
1. Prevent default form submission
2. Get input value
3. If Countku mode:
a. Validate haiku structure (5-7-5)
b. If invalid → shake, show error, return
c. Convert words to math expression
d. If conversion error → shake, show error, return
e. If null → shake, show error, return
f. exprToEvaluate = converted expression
4. Temporarily set mode to 'normal' (prevent re-conversion)
5. evaluateExpression(exprToEvaluate)
6. Restore original mode
7. Compare result to targetNumber (currentNumber + 1)
8. If match → success animation, increment
9. If no match → failure, end game
index.html # Standalone single-file application
archive/
last-stable (v1.0.0).html # Stable baseline
version-unknown (v1.1).html # Live deployment variant
version-unknown (v1.2).html # Updated variant
sakura-count-ninja-v6-*.html # v6 iterations
changelog.md # Version history
database.txt # Original specification document
engine-readme.md # This file