プログラミング練習用のタイピングゲームを作った話。エンジニアなら4000点を超えたい！

## はじめに
少しキャッチーなタイトルにしました。すみません。

あくまで、お遊び程度として是非タイピングゲームを楽しんで頂ければと思います。

以下のプログラミング練習用のタイピングゲームを作成しました。

https://untyping.jp/?from=qiita

結構面白いです。ハマります。是非、一度プレイしてみて下さい。

4000 点を超えると、結構凄いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/ad5f4acb-4397-b17e-598e-5e6252fce1f1.png)


## 目指すべきスコア
こちらのプログラミング練習用のタイピングゲームで、目標にすべきスコアを簡単にまとめました。社内で色々遊んでみて、大体こんな感じかなー、という感じで適当に書いてます。

### 8000 点以上
まだ誰も到達したことが無い未踏の地。自分はたどり着ける気がしません。

### 7000 点以上
かなりタイピングが速い人。寿司打で 20000 点以上は軽く出せる人が到達できるスコアです。

### 6000 点以上
寿司打で 20000 点を稀に超える人が到達できるスコア。


### 5000 点以上
タイピングのかなり速いエンジニアが到達できるスコア。

寿司打だと、体感 18000 点ぐらいの感覚です。

### 4000 点以上
タイピングの速いエンジニアが到達できるスコア。

4000 点を超えたら、結構凄いと思います。

### 3000 点以上
通常のエンジニアが到達できるスコア。

弊社の新米エンジニアに遊んでもらったところ、大体この辺りのスコアでした。

### 2000 点以上
通常の社会人レベル。

大体、上記のようなレベル感です。

是非、遊んでみて下さい。

https://untyping.jp/

## 作成した経緯
社内のメンバーが、タイピングゲーム大好きだからです。

あまり稼げるようなプロダクトではありませんが（そもそもお金を稼げるようなシステムすら今は無い）、社内のメンバーが楽しく遊べるようにタイピングゲームを作成しました。

後、T3 Stack と Next.js の AppRouter を使って何かプロダクトを作成してみたかったので、勉強のついでに作成しました。

社内のエンジニアには、こちらのタイピングゲームで 4000 点以上のスコアを獲得することを推奨してます。

## タイピングゲームの内容について
今現在は、**JavaScript コース**・**TypeScript コース**・**React コース**・**Python コース**の４つです。

弊社がフロントエンド重視の会社ということもあり、フロントエンドに偏重したタイピングゲーム構成となっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/a92d6644-3b98-ce6a-bdb0-c4cfa221f713.png)

ゲームの内容としては、120 秒間ひたすらタイピングをし続ける、というものです。

それぞれのコードに、一律 15 秒間の制限時間が設定されています。制限時間内に打ち終わったコードのみ、得点をゲットできます。

文字ごとにスコアが設定されており、打ちにくい文字の方が、高いスコアが設定されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/62b03a49-6006-1f71-c72e-ebd67390b901.png)

10 文字から 50 文字のコードが出現するので、一生懸命タイピングしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/d695501b-d418-e280-6188-62d6d82a501d.png)

難易度が高いコードは、タイピングが速い人でもギリギリ打ち逃してしまうぐらいの難易度になっています。

程よい緊張感があるので、飽きずに楽しめます。

## タイピングゲームの機能について
ここからは、UnTyping の機能について紹介していきます。

### ゲームの記録機能
Google 認証でログインを行うと、ゲームの記録を行うことができます。

毎回のスコアが、以下のようなグラフとして記録されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/ef6e528a-3566-c80c-3565-171a22b1e592.png)

こちらのグラフを眺めることで、成長を実感することができます。

以下のように、それぞれのコースの記録の詳細を見ることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/d21b6508-84fd-dade-1763-1c9f76ca2665.png)

また、ゲームの途中で席を離脱してしまい、おかしなデータが記録されてしまった場合は、記録を削除できます。

こちらで、総プレイ時間やベストスコア、ベストスピード、世界ランクを確認することができます。

### 世界ランキング機能
こちらのタイピングゲームには、世界ランキング機能がついています。

各々のコースごとに、世界ランキングが算出されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/747f3119-dc8a-4818-a69f-62892a7ff95f.png)

現在、アカウントを持っている方全員の世界ランキングを参照することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/745ddf6c-58bc-469a-7d20-4be66fb06ce3.png)

タイピングを練習して、世界ランキング 1 位を目指しましょう。

コースを打ち終わる毎に、そのスコアの世界ランキングが算出されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/033e0df5-1c59-68a3-17a1-7724f333a11e.png)

また、コースごとのベストスコアが、世界ランキングとして記録されます。

## 技術スタック
T3 Stack の構成で作成しました。

- Next.js
- TypeScript
- tRPC
- Prisma
- TailwindCSS
- Auth.js

という構成ですね。

ちなみに、データベースには Supabase を、デプロイ先としては Vercel を使用しています。

今回は、自社のプロダクトとしては初めて Next.js の App Router を使用しました。

今回のプロダクトで工夫したコードを紹介します。

### 認証情報の取り回し
Auth.js の認証情報の取り回しを工夫しました。

useSession を使用してデータを取得する際、セッション情報の取得に時間がかかるのが不便ですよね。


```js
const { data: session, status } = useSession();
```

上記で、session 情報の取得中に`null`が入るため、`status`を見て session 情報がどの状態にあるのかを判断する必要がありました。

App Router の サーバーコンポーネントを Recoil を使用することで、必ず session 情報が取得し終わっていることを保証するように実装しました。

以下のコードです。

```ts
import { getServerAuthSession } from '@/server/auth';

const SessionAndRecoilProvider = async ({
  children,
}: {
  children: ReactNode;
}): Promise<JSX.Element> => {
  const session = await getServerAuthSession();

  return (
    <HydrateClient>
      <RecoilProvider session={session}>{children}</RecoilProvider>
    </HydrateClient>
  );
};

export default SessionAndRecoilProvider;
```


```ts
'use client';

const RecoilProvider = (props: Props): JSX.Element => {
  const { children, session } = props;
  return (
    <RecoilRoot
      initializeState={({ set }) => {
        set(sessionState, session); // サーバーサイドで取得したセッションをセット
      }}
    >
      {children}
    </RecoilRoot>
  );
};

export default RecoilProvider;
```

上記のように書くことで、Recoil の`sessionState`からデータを取得した際に、session 情報の取得が完了していることが保証されます。

```ts
const session = useRecoilValue(sessionState);
```

上記の`session.user`を確認することで、ユーザーがログインしているかどうかを判定できます。

`session`情報の取得中のハンドリングをする必要が無いので、フロントのコードがシンプルになりました。

## 悩んだこと
今回、初めて`tRPC`を用いてプロダクトを作成しましたが、API を叩くのがいつもより遅い気がします。

Supabase のスペックをケチったのが原因なのか、`tRPC`を用いたのが原因か分かりませんが、API が結構遅いです。

特に、`protectedProcedure`を使用しているプロシージャーが、遅い感じがしますね。

今回はとりあえず、`tanStack Query`の`usePrefetchQuery`を使用することで、プロダクト自体のパフォーマンスを向上させました。

ちょっと調べてみても原因が分からなかったので、もう少し調査してみようかと思います。

## 削った機能
今回は、自分が欲しいタイピングゲームを作成しました。

いくつか、通常のタイピングゲームにはある機能を削り落としました。

そのため、音が出る機能を削除しました。

また、自分はミスタイプが多めなので、ミスタイプをした際にスコアが削られる機能を削除しました。

正確さよりも、速度重視のタイピングゲームになっています。

## 終わりに
今回は、プログラミング練習用のタイピングゲームを作成した話についてまとめました。


https://untyping.jp/?from=qiita

是非、スコア 4000 点越えを目指して頑張ってください。

### 余談
今現在、世界ランキングに名を連ねるユーザーは、殆ど社内のエンジニアです。

`numa`という社内エンジニアが、現在四冠を取得しています。

是非、打倒してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411258/726477a7-21d3-c425-7d9f-0dcf81670b55.png)