Browse Source

Initial commit

Thomas Dy 7 years ago
commit
efb0c06a97
8 changed files with 519 additions and 0 deletions
  1. 1 0
      .gitignore
  2. 9 0
      dist/index.html
  3. 14 0
      package-lock.json
  4. 14 0
      package.json
  5. 18 0
      src/index.ts
  6. 315 0
      src/kana.ts
  7. 137 0
      src/state.ts
  8. 11 0
      tsconfig.json

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+node_modules

+ 9 - 0
dist/index.html

@@ -0,0 +1,9 @@
+<html>
+  <head>
+    <title>Typing Freaks</title>
+  </head>
+  <body>
+    <div id="container"></div>
+    <script type="text/javascript" src="bundle.js"></script>
+  </body>
+</html>

+ 14 - 0
package-lock.json

@@ -0,0 +1,14 @@
+{
+  "name": "typingfreaks",
+  "version": "0.0.1",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "typescript": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz",
+      "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=",
+      "dev": true
+    }
+  }
+}

+ 14 - 0
package.json

@@ -0,0 +1,14 @@
+{
+  "name": "typingfreaks",
+  "version": "0.0.1",
+  "description": "A typing game for Japanese",
+  "scripts": {
+    "build": "tsc",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "Thomas Dy <thatsmydoing@gmail.com",
+  "license": "ISC",
+  "devDependencies": {
+    "typescript": "^2.6.2"
+  }
+}

+ 18 - 0
src/index.ts

@@ -0,0 +1,18 @@
+/// <reference path="kana.ts" />
+
+let input = 'ちっじゃう';
+let inputState = new kana.KanaInputState(input);
+
+let container = document.querySelector('#container');
+let kanaElement = document.createElement('p');
+kanaElement.textContent = input;
+container.appendChild(kanaElement);
+let romajiElement = document.createElement('p');
+romajiElement.textContent = input;
+container.appendChild(romajiElement);
+
+romajiElement.textContent = inputState.getRemainingInput();
+document.addEventListener('keydown', event => {
+  inputState.handleInput(event.key);
+  romajiElement.textContent = inputState.getRemainingInput();
+});

+ 315 - 0
src/kana.ts

@@ -0,0 +1,315 @@
+/**
+ * This module is mainly for handling romaji input to match the provided kana
+ * input. While most kana map one-to-one with romaji, some kana have multiple
+ * ways to be inputted. In addition, we also have to handle っ which causes the
+ * next consonant to be repeated.
+ *
+ * The state management is done by having a state machine for each kana and it
+ * should handle all possible variations of the romaji to be inputted.
+ * Additionally, it also keeps track of what is left to be input, and adjusts
+ * itself accordingly if an alternative romaji was used.
+ */
+
+/// <reference path="state.ts" />
+
+namespace kana {
+  import StateMachine = state.StateMachine;
+  import TransitionResult = state.TransitionResult;
+  import t = state.makeTransition;
+
+  function literal(source: string): StateMachine {
+    let transitions = [];
+    for (let i = 0; i < source.length; ++i) {
+      let from = source.substring(i);
+      let input = source.charAt(i);
+      let to = source.substring(i+1);
+      transitions.push(t(from, input, to));
+    }
+    return state.buildFromTransitions(source, transitions);
+  }
+
+  function shi(): StateMachine {
+    return state.buildFromTransitions('shi', [
+      t('shi', 's', 'hi'),
+      t('hi', 'h', 'i'),
+      t('hi', 'i', ''),
+      t('i', 'i', '')
+    ]);
+  }
+
+  function chi(): StateMachine {
+    return state.buildFromTransitions('chi', [
+      t('chi', 'c', 'hi'),
+      t('chi', 't', 'i'),
+      t('hi', 'h', 'i'),
+      t('i', 'i', '')
+    ]);
+  }
+
+  function tsu(): StateMachine {
+    return state.buildFromTransitions('tsu', [
+      t('tsu', 't', 'su'),
+      t('su', 's', 'u'),
+      t('su', 'u', ''),
+      t('u', 'u', '')
+    ]);
+  }
+
+  function fu(): StateMachine {
+    return state.buildFromTransitions('fu', [
+      t('fu', 'f', 'u'),
+      t('fu', 'h', 'u'),
+      t('u', 'u', '')
+    ]);
+  }
+
+  function ji(): StateMachine {
+    return state.buildFromTransitions('ji', [
+      t('ji', 'j', 'i'),
+      t('ji', 'z', 'i'),
+      t('i', 'i', '')
+    ]);
+  }
+
+  function sh(end: string): StateMachine {
+    let source = 'sh' + end;
+    let middle = 'h' + end;
+    return state.buildFromTransitions(source, [
+      t(source, 's', middle),
+      t(middle, 'h', end),
+      t(middle, 'y', end),
+      t(end, end, '')
+    ]);
+  }
+
+  function ch(end: string): StateMachine {
+    let source = 'ch' + end;
+    let middle = 'h' + end;
+    let altMiddle = 'y' + end;
+
+    return state.buildFromTransitions(source, [
+      t(source, 'c', middle),
+      t(middle, 'h', end),
+      t(source, 't', altMiddle),
+      t(altMiddle, 'y', end),
+      t(end, end, '')
+    ]);
+  }
+
+  function j(end: string): StateMachine {
+    let source = 'j' + end;
+    let altMiddle = 'y' + end;
+
+    return state.buildFromTransitions(source, [
+      t(source, 'j', end),
+      t(source, 'z', altMiddle),
+      t(end, 'y', end),
+      t(altMiddle, 'y', end),
+      t(end, end, '')
+    ]);
+  }
+
+  interface KanaMapping {
+    [index: string]: StateMachine
+  }
+
+  const SINGLE_KANA_MAPPING: KanaMapping = {
+    "あ": literal('a'),
+    "い": literal('i'),
+    "う": literal('u'),
+    "え": literal('e'),
+    "お": literal('o'),
+    "か": literal('ka'),
+    "き": literal('ki'),
+    "く": literal('ku'),
+    "け": literal('ke'),
+    "こ": literal('ko'),
+    "さ": literal('sa'),
+    "し": shi(),
+    "す": literal('su'),
+    "せ": literal('se'),
+    "そ": literal('so'),
+    "た": literal('ta'),
+    "ち": chi(),
+    "つ": tsu(),
+    "て": literal('te'),
+    "と": literal('to'),
+    "な": literal('na'),
+    "に": literal('ni'),
+    "ぬ": literal('nu'),
+    "ね": literal('ne'),
+    "の": literal('no'),
+    "は": literal('ha'),
+    "ひ": literal('hi'),
+    "ふ": fu(),
+    "へ": literal('he'),
+    "ほ": literal('ho'),
+    "ま": literal('ma'),
+    "み": literal('mi'),
+    "む": literal('mu'),
+    "め": literal('me'),
+    "も": literal('mo'),
+    "や": literal('ya'),
+    "ゆ": literal('yu'),
+    "よ": literal('yo'),
+    "ら": literal('ra'),
+    "り": literal('ri'),
+    "る": literal('ru'),
+    "れ": literal('re'),
+    "ろ": literal('ro'),
+    "わ": literal('wa'),
+    "を": literal('wo'),
+    "ん": literal('n'),
+    "が": literal('ga'),
+    "ぎ": literal('gi'),
+    "ぐ": literal('gu'),
+    "げ": literal('ge'),
+    "ご": literal('go'),
+    "ざ": literal('za'),
+    "じ": ji(),
+    "ず": literal('zu'),
+    "ぜ": literal('ze'),
+    "ぞ": literal('zo'),
+    "だ": literal('da'),
+    "ぢ": literal('di'),
+    "づ": literal('du'),
+    "で": literal('de'),
+    "ど": literal('do'),
+    "ば": literal('ba'),
+    "び": literal('bi'),
+    "ぶ": literal('bu'),
+    "べ": literal('be'),
+    "ぼ": literal('bo'),
+    "ぱ": literal('pa'),
+    "ぴ": literal('pi'),
+    "ぷ": literal('pu'),
+    "ぺ": literal('pe'),
+    "ぽ": literal('po')
+  }
+
+  const DOUBLE_KANA_MAPPING: KanaMapping = {
+    "きゃ": literal('kya'),
+    "きゅ": literal('kyu'),
+    "きょ": literal('kyo'),
+    "しゃ": sh('a'),
+    "しゅ": sh('u'),
+    "しょ": sh('o'),
+    "ちゃ": ch('a'),
+    "ちゅ": ch('u'),
+    "ちょ": ch('o'),
+    "にゃ": literal('nya'),
+    "にゅ": literal('nyu'),
+    "にょ": literal('nyo'),
+    "ひゃ": literal('hya'),
+    "ひゅ": literal('hyu'),
+    "ひょ": literal('hyo'),
+    "みゃ": literal('mya'),
+    "みゅ": literal('myu'),
+    "みょ": literal('myo'),
+    "りゃ": literal('rya'),
+    "りゅ": literal('ryu'),
+    "りょ": literal('ryo'),
+    "ぎゃ": literal('gya'),
+    "ぎゅ": literal('gyu'),
+    "ぎょ": literal('gyo'),
+    "じゃ": j('a'),
+    "じゅ": j('u'),
+    "じょ": j('o'),
+    "ぢゃ": literal('dya'),
+    "ぢゅ": literal('dyu'),
+    "ぢょ": literal('dyo'),
+    "びゃ": literal('bya'),
+    "びゅ": literal('byu'),
+    "びょ": literal('byo'),
+    "ぴゃ": literal('pya'),
+    "ぴゅ": literal('pyu'),
+    "ぴょ": literal('pyo')
+  }
+
+  export class KanaInputState {
+    kana: string[];
+    stateMachines: StateMachine[];
+    currentIndex: number;
+
+    constructor(input: string) {
+      let kana: string[] = [];
+      let machines: StateMachine[] = [];
+      let prevTsu = false;
+
+      // we pad the input so checking 2 at a time is simpler
+      let remaining = input.toLowerCase() + ' ';
+      while (remaining.length > 1) {
+        let nextOne = remaining.substring(0, 1);
+        if (/\s/.test(nextOne)) {
+          remaining = remaining.substring(1);
+          continue;
+        }
+
+        let nextTwo = remaining.substring(0, 2);
+        let doubleKana = DOUBLE_KANA_MAPPING[nextTwo];
+        if (doubleKana != undefined) {
+          if (prevTsu) {
+            kana.push('っ' + nextTwo);
+            machines.push(doubleKana.extend());
+            prevTsu = false;
+          } else {
+            kana.push(nextTwo);
+            machines.push(doubleKana.clone());
+          }
+          remaining = remaining.substring(2);
+        } else {
+          if (nextOne === 'っ') {
+            prevTsu = true;
+            remaining = remaining.substring(1);
+          } else {
+            let singleKana = SINGLE_KANA_MAPPING[nextOne];
+            if (singleKana != undefined) {
+              if (prevTsu) {
+                kana.push('っ' + nextOne);
+                machines.push(singleKana.extend());
+              } else {
+                kana.push(nextOne);
+                machines.push(singleKana.clone());
+              }
+            } else {
+              kana.push(nextOne);
+              machines.push(literal(nextOne));
+            }
+
+            prevTsu = false;
+            remaining = remaining.substring(1);
+          }
+        }
+      }
+
+      this.kana = kana;
+      this.stateMachines = machines;
+      this.currentIndex = 0;
+    }
+
+    handleInput(input: string): boolean {
+      if (this.currentIndex >= this.stateMachines.length) return false;
+
+      let currentMachine = this.stateMachines[this.currentIndex];
+      let result = currentMachine.transition(input);
+      switch (result) {
+        case TransitionResult.FAILED:
+          return false;
+        case TransitionResult.SUCCESS:
+          return true;
+        case TransitionResult.FINISHED:
+          this.currentIndex += 1;
+          return true;
+      }
+    }
+
+    getRemainingInput(): string {
+      let remaining = '';
+      for (let i = this.currentIndex; i < this.stateMachines.length; ++i) {
+        remaining += this.stateMachines[i].getDisplay();
+      }
+      return remaining;
+    }
+  }
+
+}

+ 137 - 0
src/state.ts

@@ -0,0 +1,137 @@
+/**
+ * This module is a simple state machine implementation. We model states as a
+ * graph, and the state machine simply keeps track of the current and final
+ * state. The state also has a display field as metadata. This serves as the
+ * state name and is also used to represent the remaining text to be input at
+ * that particular state.
+ *
+ * The only other important thing to note is the extend method. This is solely
+ * for handling っ. Essentially, it "doubles" the first letter of the current
+ * state by creating a new state and intermediate states for each transition out
+ * it has. A state with multiple transitions essentially means it has multiple
+ * different spellings, so it's important to name the intermediate states
+ * correctly.
+ */
+namespace state {
+  export enum TransitionResult { FAILED, SUCCESS, FINISHED }
+
+  interface StateMap {
+    [index: string]: State
+  }
+
+  interface StateTransitionList {
+    [index: string]: State
+  }
+
+  export class State {
+    display: string;
+    transitions: StateTransitionList;
+
+    constructor(display: string) {
+      this.display = display;
+      this.transitions = {};
+    }
+
+    addTransition(input: string, state: State): void {
+      this.transitions[input] = state;
+    }
+
+    extend(): State {
+      let extendedDisplay = this.display.charAt(0) + this.display;
+      let newState = new State(extendedDisplay);
+
+      Object.keys(this.transitions).forEach(k => {
+        let nextState = this.transitions[k];
+        let intermediateDisplay = k + nextState.display;
+        let intermediateState = new State(intermediateDisplay);
+        intermediateState.addTransition(k, nextState);
+        newState.addTransition(k, intermediateState);
+      })
+
+      return newState;
+    }
+
+    transition(input: string): State | null {
+      return this.transitions[input];
+    }
+  }
+
+  export class StateMachine {
+    initialState: State;
+    finalState: State;
+    currentState: State;
+
+    constructor(initialState: State, finalState: State) {
+      this.initialState = initialState;
+      this.currentState = initialState;
+      this.finalState = finalState;
+    }
+
+    transition(input: string): TransitionResult {
+      let newState = this.currentState.transition(input);
+      if (newState == null) {
+        return TransitionResult.FAILED;
+      } else {
+        this.currentState = newState;
+        if (this.finalState === newState) {
+          return TransitionResult.FINISHED;
+        } else {
+          return TransitionResult.SUCCESS;
+        }
+      }
+    }
+
+    isFinished(): boolean {
+      return this.currentState === this.finalState;
+    }
+
+    reset(): void {
+      this.currentState = this.initialState;
+    }
+
+    clone(): StateMachine {
+      return new StateMachine(this.initialState, this.finalState);
+    }
+
+    extend(): StateMachine {
+      let newInitialState = this.initialState.extend();
+      return new StateMachine(newInitialState, this.finalState);
+    }
+
+    getWord(): string {
+      return this.initialState.display;
+    }
+
+    getDisplay(): string {
+      return this.currentState.display;
+    }
+  }
+
+  interface Transition {
+    from: string,
+    input: string,
+    to: string
+  }
+
+  export function buildFromTransitions(initial: string, transitions: Transition[]): StateMachine {
+    let states: StateMap = {};
+    function getState(name: string): State {
+      if (states[name] === undefined) {
+        states[name] = new State(name);
+      }
+      return states[name];
+    }
+    transitions.forEach(t => {
+      let fromState = getState(t.from);
+      let toState = getState(t.to);
+      fromState.addTransition(t.input, toState);
+    })
+    let initialState = getState(initial);
+    let finalState = getState('');
+    return new StateMachine(initialState, finalState);
+  }
+
+  export function makeTransition(from: string, input: string, to: string): Transition {
+    return { from, input, to };
+  }
+}

+ 11 - 0
tsconfig.json

@@ -0,0 +1,11 @@
+{
+  "compilerOptions": {
+    "noImplicitAny": true,
+    "removeComments": true,
+    "sourceMap": true,
+    "outFile": "dist/bundle.js"
+  },
+  "include": [
+    "src/**/*"
+  ]
+}