フロントエンド うえむーのブログサイト | NU-Blog(エヌ・ユーブログ)

どうも、うえむーです。

Golangでタイピングゲームを作ってつまづいたことをお話したいと思います。
5/4に「Golangでタイピングゲームを作ってつまづいたこと」を投稿しますと言いながら、
2ヶ月以上経ちました。。。遅くなりまして申し訳ありません。。。

前回投稿したものはこちらになりますので読んでみてください!

※golangでタイピングゲームを作ってみた 。その1
https://nu-blogsite.net/blog/business/detail/?date_id=udxptiy93

さて、そろそろ本題に入ります。

タイピングゲームを作ったきっかけ、挑戦したこと


僕はgo langでタイピングゲームを作ろうとしたきっかけは「progate」です。
学習コースに「golang タイピングゲーム作成」がありましたので、基礎を学び・簡単なタイピングゲームを作成しました。

作成したのですが、物足りないと感じ以下の機能を追加して個人の環境で作成しました。

・問題をランダム表示
・制限時間付き


この機能を追加して実装するのですが。。。色々とつまづいた事が沢山ありました。
つまづいたことを2つお話していきたいと思います。

タイピングゲームを作成してつまづいた事 その1「Scan関数の罠。。。」

最初はタイマーなし・ランダム表示機能なしでタイピングゲーム作成しました。
コードは以下のような感じです。

package main
import (
    "fmt"
    "os"
    "bufio"
)

func main() {
    totalScore := 0
    // 引数にtotalScoreのポインタを渡してください
    ask(1, "dog", &totalScore)
    ask(2, "cat", &totalScore)
    ask(3, "fish", &totalScore)
    ask(4, "Tiger", &totalScore)
    ask(5, "Elephant", &totalScore)
    ask(6, "Crocodile", &totalScore)
    ask(7, "this is a dog", &totalScore)
    ask(8, "Amphibians", &totalScore)
    ask(9, "Butterfly", &totalScore)
    ask(10, "excellentswimmer", &totalScore)

    fmt.Println("スコア", totalScore)
    if totalScore <= 30 {
      fmt.Println("頑張りましょう!")
    } else if totalScore <= 60 && totalScore >= 31 {
      fmt.Println("もう少しです!")
    } else if totalScore <= 80 && totalScore >= 61 {
      fmt.Println("なかなかいいですね!")
    } else if totalScore >= 81 {
      fmt.Println("素晴らしい!!")
    }
}
// 渡されるtotalScoreのポインタを受け取るように変更してください
func ask(number int, question string, scorePtr*int) {
    var ans string
    fmt.Printf("[質問%d] 次の単語を入力してください: %s\n", number, question)
    fmt.Scan(&ans)
    if question == ans {
        fmt.Println("正解です!")
        // ポインタを使って加算してください
        *scorePtr += 10        
    } else {
        fmt.Println("不正解です!")
    }
}


Progateを参考にしてプログラミンングしたのはいいんですが、思いもよらないバグが発生しました。
タイピングゲームの7問目に半角スペースを含めたワード「this is a dog」と設定し、実行すると。。。

    ask(7, "this is a dog", &totalScore)





正解したはずなのに、「不正解です!」と返され8問目〜10問目が飛ばされてしまうバグが発生してしましました。。。

「なぜだ。。。」と思いgoogleで調査してみました。

原因はScan関数らしく、ドキュメントを見ると以下のことが書かれておりました。。。



つまり、自動で空白区切りで返ってくるというわけだったんですね。。。
色々と調べて以下のように書き直しました。

訂正前:

// 渡されるtotalScoreのポインタを受け取るように変更してください
func ask(number int, question string, scorePtr*int) {
    var ans string
    fmt.Printf("[質問%d] 次の単語を入力してください: %s\n", number, question)
    fmt.Scan(&ans)
    if question == ans {
        fmt.Println("正解です!")
        // ポインタを使って加算してください
        *scorePtr += 10        
    } else {
        fmt.Println("不正解です!")
    }
}


訂正後:

// 渡されるtotalScoreのポインタを受け取るように変更してください
func ask(number int, question string, scorePtr*int) {
    var ans string
    fmt.Printf("[質問%d] 次の単語を入力してください: %s\n", number, question)
    sc := bufio.NewScanner(os.Stdin)
    if sc.Scan() {
       ans = sc.Text()
    }
    if question == ans {
        fmt.Println("正解です!")
        // ポインタを使って加算してください
        *scorePtr += 10        
    } else {
        fmt.Println("不正解です!")
    }
}


標準入力からテキストを一行ずつ読み込む「bufio.NewScanner」の関数 を利用しました。
標準入力から読み込んだテキストをスキャンできたら、テキストを文字列に変更し「ans 変数」に渡して、
「ans 変数」と問題が同一であれば「正解です!」という文字列を出力 + ポイント加算し、
同一でなければ「不正解です!」 という文字列を出力するように処理に変更しました。

その結果、「this is a dog」と入力しても、先ほどみたいに不具合ににならず正常に 処理ができました。



タイピングゲームを作成してつまづいた事 その2「制限時間設定。。。」

不具合解消したので、上記に記載した通り以下を実装しました。

・問題をランダム表示
・制限時間付き


最初は「問題をランダム表示」の実装に入りました。
shuffle関数を利用すれば、問題をランダム表示することができました。

// 配列をシャッフルする
func shuffle(data []string) {
    n := len(data)
    rand.Seed(time.Now().Unix())
    for i := n - 1; i >= 0; i-- {
        j := rand.Intn(i + 1)
        data[i], data[j] = data[j], data[i]
    }
}


次は、タイマー設定の実装です。
他の記事を参考にすると以下の関数を入れればタイマー設定できるようです。

func init() {
    //オプションで制限時間をできる
    flag.IntVar(&t, "t", 1, "制限時間(分)")
    flag.Parse()
}


だが、いくら頑張っても
error...
error...
error...

本当につまづきましたね。。。
なので、他の参考記事・ドキュメントを調べて、
大幅に変更してなんとか達成できました。。。
修正後のコードはいかになります。

修正後:

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
    "time"
    "math/rand"
)

// 配列をシャッフルする
func shuffle(data []string) {
    n := len(data)
    rand.Seed(time.Now().Unix())
    for i := n - 1; i >= 0; i-- {
        j := rand.Intn(i + 1)
        data[i], data[j] = data[j], data[i]
    }
}

var t int

func init() {
    //オプションで制限時間をできる
    flag.IntVar(&t, "t", 1, "制限時間(分)")
    flag.Parse()
}

func main() {
    var (
        ch_rcv    = myinput(os.Stdin)
        tm        = time.After(time.Duration(t) * time.Minute)
        words     = []string{ "raccoon", "dog", "wild boar"。。。 }
        score     = 0
    )
    fmt.Println()

    shuffle(words);
    fmt.Println("タイピングゲームを始めます。制限時間は", t, "分。1語1点、", len(words), "点満点")
    //送信用チャネル
    num := 1
    for i := true; i && score < len(words); {
        question := words[score]
        fmt.Print("[質問", num ,"]次の単語を入力してください:", question, "\n")
        fmt.Print("[答え]")
        select {
        case x := <-ch_rcv:
            //標準入力に何か入力された時の処理
            // 入力された文字が一致しているかどうかをチェックする
            if question == x {
                fmt.Println("正解です!")
                score++
                num++
            } else {
                fmt.Println("不正解です!")
            }
        case <-tm:
            //制限時間が過ぎた際の処理
            fmt.Println("\n制限時間を過ぎました")
            i = false
        }
    }
    fmt.Println("あなたの点数:", score, "点 / ", len(words), " 点")
    if score <= 10 {
      fmt.Println("判定 F")
    } else if score <= 15 && score > 10 {
      fmt.Println("判定 E")
。。。
    } else if score <= 45 && score > 40 {
      fmt.Println("判定 SS")
    } else if score > 45 {
      fmt.Println("判定 SSS")
    }
}

func myinput(r io.Reader) <-chan string {
    // サブgo ルーチン
    // 標準入力から受け取った文字列を標準出力へ出力する
    ch1 := make(chan string)
    go func() {
        s := bufio.NewScanner(r)
        for s.Scan() {
            ch1 <- s.Text()
        }
    }()
    return ch1
}


修正前は、main関数・ask関数を分けて実装しましたが、main関数は、ask関数に連番・お題・スコアの変数を設定し、Printfで結果のコメントを出力するコードを入れました。
ask関数は変数totalScoreのポインタを引数に指定して、正解の場合は、ポインタを使って元の変数に10を足す処理を入れました。

修正後は、main関数、ask関数を分けるのをやめました。
main関数で一つにし、if文の条件指定からswitch文と似ているselect文に変更して、正解の場合は、10を足す処理を入れて、時間切れの場合は強制終了する処理を入れました。

その結果、うまく正常に実装できました。。。。



go langはなれないので少し難しかったですが、いい刺激になりました。
落ち着いたらこれを応用化してフロントに反映したいと思ってます。

それではまた!