スマレジエンジニアyushiのブログ

スマレジエンジニアのブログ

【タイピングゲーム 第5回】"ん"に対応する

タイピングゲームを作っています。

yushi-dev.hatenablog.com

技術スタックは下記の通りです。

  • Vue.js3
  • Vite

今回は、"ん"に対応します。

"ん"への対応

"ん"が特殊な動きをするのは、例えば"あんこ"と入力するときに、"annko"という入力の他、"anko"という入力もできることです。
つまり、"n"を1回の入力で済ませられるケースがあります。

これに対応する実装が、下記の通りです。

    switch (textArray.value[inputIndex.value]) {
      case 'ん':
        if (
          inputCurrentLetter.value === 'n'
          && key !== 'n'
          && textAlphabetArray.value.length >= inputIndex.value + 1
          && !['a', 'i', 'u', 'e', 'o', 'n']
            .includes(textAlphabetArray.value[inputIndex.value + 1]
              .map((item: string) => item[0])
            )
          && textAlphabetArray.value[inputIndex.value + 1]
            .map((item: string) => item[0])
            .includes(key)
        ) {
          inputTextArray.value.push(inputCurrentLetter.value)
          inputCurrentLetter.value = key
          return true
        }
    }

つまり、
現在の入力ひらがなが"ん"で、
nを一回入力済みで、
今回の入力値がnではなくて、
次の入力ひらがなが存在し、
そのアルファベットの最初の文字が母音や"n"ではなく、
そのアルファベットの最初の文字が入力された場合に、 1つのnのみで"ん"を入力できます。

Pull Request

https://github.com/nek0meshi/typing/pull/5

まとめ

ゴリゴリとしたロジックが増えてきました。

引き続き頑張ります。

【タイピングゲーム 第4回】ひらがなに対応する

タイピングゲームを作っています。

yushi-dev.hatenablog.com

技術スタックは下記の通りです。

  • Vue.js3
  • Vite

前回までは英字のみに対応していましたが、今回はひらがな対応をします。

ひらがな対応

ひらがなのローマ字打ちに対応していくのですが、重要なポイントは、ひらがなの打ち込み方は一意ではない、ということです。

例えば、

  • し -> si, shi
  • つ -> tu, tsu

など、複数の入力方式があります。これへの対応は結構大変です。

今回は対応Mapを作ることで対応していきます。

const LETTER_MAP = {
  'あ': ['a'],
  'い': ['i'],
  'う': ['u'],
  'え': ['e'],
  'お': ['o'],
  'か': ['ka'],
  'き': ['ki'],
  'く': ['ku'],
  'け': ['ke'],
  'こ': ['ko'],
  'さ': ['sa'],
  'し': ['si', 'shi'],
  'す': ['su'],
  'せ': ['se'],
  'そ': ['so'],
  'た': ['ta'],
  'ち': ['ti', 'chi'],
  'つ': ['tu', 'tsu'],
  'て': ['te'],
  'と': ['to'],
  'な': ['na'],
  'に': ['ni'],
  'ぬ': ['nu'],
  'ね': ['ne'],
  'の': ['no'],
  'は': ['ha'],
  'ひ': ['hi'],
  'ふ': ['hu', 'fu'],
  'へ': ['he'],
  'ほ': ['ho'],
  'ま': ['ma'],
  'み': ['mi'],
  'む': ['mu'],
  'め': ['me'],
  'も': ['mo'],
  'や': ['ya'],
  'ゆ': ['yu'],
  'よ': ['yo'],
  'ら': ['ra'],
  'り': ['ri'],
  'る': ['ru'],
  'れ': ['re'],
  'ろ': ['ro'],
  'わ': ['wa'],
  'を': ['wo'],
  'ん': ['nn'],
}

前回までは、入力値は

  const input = ref('')

という一つの変数で管理していましたが、今回以降は、

  const inputTextArray = ref<string[]>([])

では、ひらがなの単位で管理し、

  const inputCurrentLetter = ref('')

では、ひらがな一語内のローマ字を管理します。

他にも色々追記していますが、あとは地道にロジックを書いていく感じでした。

Pull Request

https://github.com/nek0meshi/typing/pull/4

まとめ

今回で一番基本的な平仮名には対応しましたが、"ん"や、撥音、拗音への対応はさらに複雑です。

引き続き頑張ります。

【タイピングゲーム 第3回】Composition APIで整理する

タイピングゲームを作っています。

yushi-dev.hatenablog.com

今回は、Composition APIを導入します。

技術スタックは下記の通りです。

  • Vue.js3
  • Vite

Composition APIについて

Composition APIについては、過去記事で言及しています。
Vue3の目玉機能の一つかなと思います。

yushi-dev.hatenablog.com

yushi-dev.hatenablog.com

composableの実装

2つのcomposableを作成しました。

1. use-text-generator

import { ref, computed } from 'vue'

const DUMMY_TEXTS = [
  // サンプルデータ.
  'inu',
  'tako',
  'saru',
  'kuni',
  'kutsu',
  'kusa',
  'matsu',
  'kame',
  'tai',
  'teko',
  'tora',
  'gyuunyuu',
  'mitsubachi',
  'rakko',
  'koara',
  'ramune',
  'neko',
  'koshiann',
  'amaguri',
]

export default function useTextGenerator(texts: string[] = DUMMY_TEXTS) {
  const usedIndexes = ref<number[]>([])

  const remainingIndexes = computed(
    () => [...Array(texts.length).keys()]
      .filter((i) => !usedIndexes.value.includes(i))
  )

  const generate = () => {
    const index = remainingIndexes.value[
      Math.floor(Math.random() * remainingIndexes.value.length)
    ]
    usedIndexes.value.push(index)

    if (remainingIndexes.value.length === 0) {
      reset()
    }

    return texts[index]
  }

  const reset = () => {
    usedIndexes.value = []
  }

  return {
    generate,
    reset,
  }
}

ソースにハードコードしたテキスト(DUMMY_TEXTS)を、ランダム順で返却する機能です。
現在のロジックでは、ローマ字にのみ対応しています。

2. use-text-typer

import { ref, computed } from 'vue'

export default function useTextTyper() {
  const text = ref('')
  const input = ref('')

  const remainingText = computed(() => text.value.slice(input.value.length))

  const set = (_text: string) => {
    text.value = _text
    input.value = ''
  }

  const type = (key: string) => {
    if (key !== remainingText.value.slice(0, 1)) {
      return false
    }

    input.value += key

    return true
  }

  return {
    // data
    text,
    input,

    // computed
    remainingText,

    // methods
    set,
    type,
  }
} 

こちらは、一つ一つのテキストについて、その入力状況を管理する機能です。

Pull Request

https://github.com/nek0meshi/typing/pull/3

まとめ

やっぱり、vueファイルからロジックを独立させられるのは可読性が高まって便利です。

【タイピングゲーム 第2回】タイマーを設置する

タイピングゲームを作っています。

yushi-dev.hatenablog.com

今回は、タイマーを設置します。

タイマー機能

タイマーを設置して、「決められたテキストを、時間内に打ち込む」というゲームにアップグレードします。

タイマー機能は、setIntervalを利用しつつ、Composition APIで実装します。

import { ref, computed } from 'vue'

const INTERVAL_TIME = 10

export default function useTimer() {
  const startTime = ref(0)
  const intervalId = ref(0)
  const seconds = ref(0)
  const timerTime = ref(0)

  const currentTime = computed(() => {
    const value = seconds.value * 1000 - (timerTime.value - startTime.value)

    return value >= 0 ? value : 0
  })
  const currentTimeSeconds = computed(() => Math.ceil(currentTime.value / 1000))

  const start = (_seconds: number, callback = () => {}) => {
    seconds.value = _seconds
    startTime.value = Date.now()

    intervalId.value = setInterval(() => {
      timerTime.value = Date.now()
      if (currentTime.value === 0) {
        // タイムアップ.
        clearInterval(intervalId.value)
        callback()
      }
    }, INTERVAL_TIME)
  }

  return {
    seconds,
    currentTime,
    currentTimeSeconds,
    start,
  }
}

startメソッドでタイマーを開始し、終了したらcallbackを呼び出します。

呼び出し元はこんな感じです。

const timer = useTimer()

timer.start(TIMER_TIME, () => {
  timeUp()
})

使いまわせそうで、Composition APIの威力を感じますね。

画面

赤枠の箇所に、残り時間を表示しています。

f:id:yushi0:20210905230259p:plain

Pull Request

https://github.com/nek0meshi/typing/pull/2

あとがき

久しぶりにリモート飲み会をしました。

【タイピングゲーム 第1回】タイピングゲームを作る

タイピングゲームを作っていこうと思います。

学生時代に少しタイピングゲームにハマっていた時期があったのですが、
当時遊んでいたゲームたちはFlash Playerとともに死んでしまいました...。

夜の森タイピング neutralx0.net

皿打 neutralx0.net

僕は絵が描けないので同じようなものは作れそうにないですが、
勉強も兼ねて簡単なタイピングゲームを作ってみようかと思います。

技術要素

今回もVue3を使っていきます。

これまでより上手くComposition APIやTypeScriptを使えたらいいなと思っています。

実装

本体のVueファイルはこのような内容です。

<template>
  <div>
    <div v-if="gameStatus === GAME_STATUS_INITIAL">
      <h1>Enterキーを押して, ゲームを開始ください。</h1>
    </div>
    <div v-else-if="gameStatus === GAME_STATUS_FINISHED">
      <h1>ゲームが終了しました。</h1>
      <p>Enterキーを押してください。</p>
    </div>
    <div v-else>
      <h1>{{ currentText }}</h1>

      <p>{{ currentInput }}</p>
    </div>
  </div>
</template>

<script lang="ts">
import { ref, computed, onMounted, defineComponent } from 'vue'

const GAME_STATUS_INITIAL = 0
const GAME_STATUS_RUNNING = 1
const GAME_STATUS_FINISHED = 2

export default defineComponent({
  setup: () => {
    const gameStatus = ref(GAME_STATUS_INITIAL)
    const texts = ref([])
    const currentIndex = ref(0)
    const currentInput = ref('')
    const currentText = computed(() => texts.value[currentIndex.value] || null)
    const currentRemainingText = computed(() => (currentText.value || '').slice(currentInput.value.length))

    const start = () => {
      gameStatus.value = GAME_STATUS_RUNNING
      texts.value = [
        // サンプルデータ.
        'poppusinanaide',
        'soratobusakana',
        'karasuhamassiro',
      ]
    }
    const reset = () => {
      gameStatus.value = GAME_STATUS_INITIAL
    }

    const onKeyInput = (e: KeyboardEvent) => {
      switch (gameStatus.value) {
        case GAME_STATUS_INITIAL:
          if (e.key === "Enter") {
            start()
          }
          break
        case GAME_STATUS_RUNNING:
          if (e.key === currentRemainingText.value.slice(0, 1)) {
            currentInput.value += e.key
          }
          if (currentRemainingText.value.length === 0) {
            // 現在のテキストの入力完了
            currentInput.value = ''
            if (currentIndex.value + 1 === texts.value.length) {
              // ゲーム終了
              gameStatus.value = GAME_STATUS_FINISHED
            }

            currentIndex.value++
          }
          break
        case GAME_STATUS_FINISHED:
          if (e.key === "Enter") {
            reset()
          }
          break
      }
    }

    onMounted(() => {
      window.addEventListener('keydown', onKeyInput)
    })

    return {
      // constants
      GAME_STATUS_INITIAL,
      GAME_STATUS_RUNNING,
      GAME_STATUS_FINISHED,

      // data
      gameStatus,
      texts,
      currentIndex,
      currentInput,
      currentText,

      // computed
      currentRemainingText,

      // methods
      onKeyInput,
    }
  }
})
</script>

今回は、用意した3つのセンテンスを入力したらそれで終了、と言う簡単なルールにしています。

初期状態・ゲーム中・終了の3状態の画面を用意し、切り替えています。

テキストフィールドを使わずに、キーイベントを取得しています。
入力判定や表示の装飾を柔軟に行うためです。

画面

スタート画面

f:id:yushi0:20210822234841p:plain

ゲーム画面

f:id:yushi0:20210822234856p:plain

Pull Request

https://github.com/nek0meshi/typing/pull/1

後記

徐々に機能拡張していこうと思います。

共通認識としての「リーダブルコード」

ここ数年をプログラマとして過ごしてきた人で、「リーダブルコード」の名前を知らない人はあまりいないんじゃないかと思います。

 

 

その名の通り、プログラミングにおいていかに読みやすいコードを書くか(そしてそれがいかに重要であるか)ついて、とても具体的に記述されています。

今回の記事は、その内容以上に、著名であり多くのエンジニアが読んでいること自体が、リーダブルコードの重要性だという話です。

 

一貫性

プログラムの可読性において、特に重要なことの一つは「一貫性」だと考えています。

コーディング規約(例えばインデントサイズ、どこにスペースを入れ、どこに改行を入れるかなど)はソースコードの全体において共通しているべきです。

あるいは並列関係にあるロジック、例えば複数のWeb APIにおいて、あるものはControllerクラスに全てのロジックが記述され、あるものはControllerクラスからRepositoryクラスを呼び出すようなレイヤード構造になっている、などの差異がある状態は避けるべきです。

 

この一貫性が守られていることのメリットは、単純に整頓されることに留まりません。

一貫性が担保されていれば、一部分を読むだけでも同様の記述があるであろう多くのコードを読み飛ばせるようになるのです。

逆に一貫性がないと、記述時にもどのルールに合わせれば良いか混乱しますし、毎度毎度どういう書き方が良いか考え直さなくてはならなくなります。


共通認識

一貫したコードを書き続けるためには、開発メンバーが共通認識を持っている必要があります。

実際には、これは簡単なことではありません。

プログラムが「動くかどうか」には絶対的な判断ができますが、プログラムが「読みやすいかどうか」については、どうしても主観的な判断になりがちです。

エンジニアはそれぞれバックグラウンドが異なりますし、それによって、「読みやすさ」の判断にはどうしてもばらつきがあります。

各メンバーが読みやすさを目指すほど、時として意見が割れる事があります。

だからと言って、各人が自分の思う「読みやすさ」を目指せば「一貫性」が失われてしまいます。

 

開発が長く続くほどメンバーも入れ替わる、といった問題もあります。

 

有名書籍が共通認識を生む

ではどうやって共通認識を得るか。

そのために有用なのが、リーダブルコードのような有名書籍です。

すなわち、全メンバーがこの書籍を読むことで、共通認識を得ることができる、ということです。

こういった書籍は既に読んでいる人が多いですし、プロジェクト参加時に新たに読むとしても、その人のキャリアにおいて決して損になりません。

もちろん、リーダブルコードに全てが書かれているわけでもなく、より発展的な、または具体的な事柄において、意見が対立することはままあると思います。

しかしそんな場合でも、少なくとも全ての議論はリーダブルコードの内容をベースにして行うことができます。

リーダブルコードにの記述に従おう、という結論で、議論をスキップできることも多いと思います。

 

具体的な例としてリーダブルコードを挙げましたが、別にこれにこだわるわけではありません。

チームで課題図書が設定できていれば良いし、あるいはドキュメントが積みあげられていれば良いかもしれません。

またこの話は、プログラム以外において、一般的に、その分野における有名書籍を読むべきという結論に拡張できると思います。

  

【将棋盤 第13回】TypeScriptのclassのテストコードを記述してみる

コツコツ将棋盤を作ってます。

使用技術

  • Vue.js3
  • Vite
  • SCSS

yushi-dev.hatenablog.com

公開ページ

https://nek0meshi.github.io/shogi-board/

今回は、vueファイルのテストコードを書いてみます。

ライブラリの導入

前回の記事で導入済みです。

yushi-dev.hatenablog.com

(@types/jest, ts-jest, typescript)

テストコードの記述

import { Piece, PieceType } from '../src/pieces'

describe('pieces.ts', () => {
  it('駒の生成1', () => {
    const piece = new Piece('king', true, 5, 1)
    expect(piece.id).toBe(1)
    expect(piece.type).toBe('king')
    expect(piece.isFirst).toBe(true)
    expect(piece.column).toBe(5)
    expect(piece.row).toBe(1)
    expect(piece.name).toBe('玉')
    expect(piece.movableList).toEqual([
      [-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1],
    ])
    expect(piece.canPromote).toBe(false)
  })

  it('駒の生成2', () => {
    const piece = new Piece('king', false, 4, 4)
    expect(piece.id).toBe(2)
    expect(piece.type).toBe('king')
    expect(piece.isFirst).toBe(false)
    expect(piece.column).toBe(4)
    expect(piece.row).toBe(4)
    expect(piece.name).toBe('王')
    expect(piece.movableList).toEqual([
      [-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1],
    ])
    expect(piece.canPromote).toBe(false)
  })
})

toBeはプリミティブ型に対してしか使えず、配列に対してはtoEqualを使う必要があります。

Pull Request

https://github.com/nek0meshi/shogi-board/pull/23

まとめ

テストコードの記述を進めています。

TypeScriptで型もあり、テストコードもあり、となるとだいぶ安心感がありますね。