【Go言語】Goで作るタイピングゲーム

## はじめに
今回は、Goで何か作りたいと無性に思い立ち、タイピングゲームを作ることにしました。この記事では、その成果物とソースコードの紹介をしていきます。
フロントエンドは全く作っていないため、ターミナル上でタイピングする感じになっています🫡

## 成果物の紹介
以下が今回作ったものになります。
![スクリーンショット 2025-01-01 15.58.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763692/b4182e9b-fd4e-1362-6498-685badf2b684.png)


https://github.com/anton-fuji/Go-typing

紹介用で制限時間を15秒にしてありますが、実際は60秒間タイピングの練習ができるようになっています。
打ち間違えがなければ「**OK!**」、間違えていれば「**NG**」と出力されます。
経過時間も表示されるようになっており、スピードを意識したタイピング練習ができます。
最後に、獲得スコアの表示と「**Finished!**」が出力されます。

では、使ったライブラリなどの紹介に移ります。

## ライブラリ紹介
今回、タイピングする文字を出力するにあたって自動で生成してくれるライブラリはないかと探していたとろこ、「**[babble](https://github.com/tjarratt/babble)**」というものを発見しました。
こちらは、英文字をランダムで生成してくれるライブラリで、Usageもわかりやすかったので、こちらを採用しました。

続いては、ソースコードを見ていきましょう。
Goの環境がある方は実際に触っていただきたいです！
## ソースコード
以下が、作成したものになります。

```golang:main.go
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"
	"time"

	"github.com/tjarratt/babble"
)

func main() {
	var (
		ch_rcv = myInput(os.Stdin)
		t      = 60
		tick   = time.NewTicker(30 * time.Second)
		done   = make(chan bool)
		n      = 0
	)
	// 文字をランダムで生成
	babbler := babble.NewBabbler()
	babbler.Count = 1

	fmt.Printf("Start the typing game. Time limit is %d seconds. Let's Start!\n", t)

	// 30秒経過後の残り時間表示のためのゴルーチンを開始
	go func() {
		for {
			select {
			case <-tick.C:
				t -= 30
				if t > 0 {
					fmt.Printf("%d seconds left\n", t)
				}
			case <-done:
				return
			}
		}
	}()

OuterLoop:
	for {
		q := babbler.Babble()
		fmt.Println(q)

		select {
		case <-time.After(time.Duration(t) * time.Second):
			fmt.Printf("Finished! Your score is %d points! Good job\n", n)
			done <- true
			break OuterLoop
		case x := <-ch_rcv:
			x = strings.TrimSpace(x)
			if x == q {
				fmt.Println("OK!")
				n += 1
			} else {
				fmt.Println("NG")
			}
		}
	}
}

func myInput(r io.Reader) <-chan string {
	ch := make(chan string)
	go func() {
		s := bufio.NewScanner(r)
		for s.Scan() {
			ch <- s.Text()
		}
		if err := s.Err(); err != nil {
			fmt.Fprintln(os.Stderr, "Read Error", err)
		}
		close(ch)
	}()
	return ch
}
```

順に見ていきましょう。
### main関数
```golang
func main() {
    var (
        ch_rcv = myInput(os.Stdin) // 標準入力をチャネルとして受け取る
        t      = 60                // 制限時間を60秒に設定
        tick   = time.NewTicker(30 * time.Second) // 30秒ごとに残り時間を表示するためのタイマー
        done   = make(chan bool)  // ゴルーチンを終了するためのチャネル
        n      = 0                // スコアを初期化
    )
    // ランダムな文字列を生成
    babbler := babble.NewBabbler()
    babbler.Count = 1
```
ここでは、標準入力を受け取るチャネル、制限時間、タイマー、終了シグナル、スコアの初期設定を行っています。
また、`babble`ライブラリを使ってランダムな文字列を生成するバブラーを設定しています。

### 残り時間表示のゴルーチン
```golang
go func() { for { select { case <-tick.C: t -= 30 if t > 0 { fmt.Printf("%d seconds left\n", t) } case <-done: return } } }()
```
30秒ごとに残り時間を表示するゴルーチンを開始しています。ゴルーチンは非同期で動作し、30秒経過ごとに残り時間を表示します。

### ゲームループ
```golang
OuterLoop:
    for {
        q := babbler.Babble() // ランダムな文字列を生成
        fmt.Println(q)

        select {
        case <-time.After(time.Duration(t) * time.Second):
            fmt.Printf("Finished! Your score is %d points! Good job\n", n)
            done <- true
            break OuterLoop
        case x := <-ch_rcv:
            x = strings.TrimSpace(x)
            if x == q {
                fmt.Println("OK!")
                n += 1
            } else {
                fmt.Println("NG")
            }
        }
    }
}
```
ここでは、ランダムな文字列を生成し、ユーザーの入力を待っています。ユーザーが入力した文字列が一致する場合は「OK!」、一致しない場合は「NG」と表示され、スコアが増減します。
制限時間が過ぎるとゲームが終了し、スコアを表示します。

### 標準入力の取得
```golang
func myInput(r io.Reader) <-chan string {
    ch := make(chan string)
    go func() {
        s := bufio.NewScanner(r)
        for s.Scan() {
            ch <- s.Text()
        }
        if err := s.Err(); err != nil {
            fmt.Fprintln(os.Stderr, "Read Error", err)
        }
        close(ch)
    }()
    return ch
}
```
この関数は、標準入力を非同期で受け取り、チャネルに送信します。
無限ループでユーザーの入力を待ち続け、入力されたテキストをチャネルに送信します。

以上がコードの全体像になります！

## まとめ
今回は、Go言語を使ってターミナル上で動作するタイピングゲームを作成しました。今後は、Next.jsなどを使ってこのゲームのフロントエンドをよりリッチにしていくことも考えています。

最後までご覧いただき、ありがとうございました！