Go x Hit&Blow

第3回 Goもくもく会

2/15(月)、株式会社エウレカさんが主催するGoもくもく会に参加してきました。

会場は明るい雰囲気でとても清潔感があり、エンジニアにとっては開発し易い環境だなぁと思いました。

こういった環境作りって大事ですよね。

eure.connpass.com

プログラム

最初の2時間がGOをもくもくといじる。後半1時間がLT&懇親会というプログラムで、

前半2時間は、もくもくするか、特定のお題目に沿ったプログラムを作るといった内容でした。

今回のお題目は「hit and blow」のプログラムを組むというもので、

駆け出しGoプレイヤーの私は勉強がてらこのお題目にチャレンジしてみる事にしました。

四苦八苦しながらでしたが、どのように実装したのか紹介したいと思います

Hit andBlow

hit and blowとは

4つの数字を互いに当てあうゲーム

ルール

  • お互いランダムな4桁の数値を用意
  • 数値は0〜9までの数値で同じ数値は使えない
  • 先攻の人が適当な数字を4つ言う
  • 後攻の人は○個の数字の位置があっていたら○ヒットという
  • 位置は違うけど△個数字が入っていたら△ブローという

(例)自分の数字が「1830」で「1248」と言われたら、「1」は場所が合ってるので1ヒット、場所は違うけど8が入っているので1ブローなので、「1ヒット1ブロー」という

実装よりも最適なアルゴリズムを考えるのが肝かな?と思ってましたが、GO初心者の私にとっては実装だけで数時間かかってしまいました汗

実装すべき機能要件は上記。1つずつ紐解いて実装していこうと思います。まずは、、、

  • 0〜9のランダムな4桁の数値
  • 同じ数値は使えない

この要件を満たす部分を実装しました。

package main

import (
    "fmt"
    "math/rand"
    "sort"
    "time"
)

func main() {
    answer := randGenerate(4)
    fmt.Println(answer)
}

func randGenerate(numLen int) []int {

    // システムが自動発行する数値
    var num int
    // 発番したn桁の乱数配列
    var nums []int

    rand.Seed(time.Now().UnixNano())
    for len(nums) < numLen {
        if len(nums) == 0 {
            nums = append(nums, rand.Intn(10))
        } else {
            num = rand.Intn(10)
            sort.Ints(nums)

            i := sort.Search(len(nums), func(i int) bool { return nums[i] >= num })
            if i < len(nums) && nums[i] == num {
                // numが既にnumsにあればリトライ
                continue
            }
            nums = append(nums, num)
        }
    }

    // 並び替え(シャッフル)
    n := len(nums)
    for i := n - 1; i >= 0; i-- {
        j := rand.Intn(i + 1)
        nums[i], nums[j] = nums[j], nums[i]
    }

    return nums
}

PHPが長かった私にとっては「ランダムな一意な4桁数値を発番」を実装するのにひと苦労。よく使う関数やアルゴリズムはライブラリ化していくなり何なりで足元固めていく感じかなーっていう印象でした。 とにもかくにも乱数ジェネレータが出来たのでここまでで一旦動作確認

$ go run main.go 
[1 5 6 9]
$ go run main.go 
[1 0 8 7]
$ go run main.go 
[6 9 1 2]
$ go run main.go 
[6 9 0 5]
$ go run main.go 
[6 7 8 4]
  • ランダムな4桁の数値
  • 数値は0〜9までの数値で同じ数値は使えない

問題なく動作しました。ここで一気に作り込み。

残りの入力値チェックと、ヒット&ブロウの突き合わせ部分を実装してみました。

package main

import (
    "bufio"
    "fmt"
    "math/rand"
    "os"
    "sort"
    "strconv"
    "time"
)

// 当てる数値の桁数
const NumLength = 4 

func main() {
    // トライ数
    var tryCnt int = 1 

    // ランダムな数値4桁発行
    answer := randGenerate(NumLength)

    // プロンプト表示
    q := fmt.Sprintf("%d桁の数値を入力してね", NumLength)
    fmt.Println(q)

    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {

        // システムが発行した乱数に合わせてユーザー入力値を配列に変換
        runes := []rune(scanner.Text())
        var arr []int

        for i := 0; i < len(runes); i++ {
            if j, err := strconv.Atoi(string(runes[i:(i + 1)])); err != nil {
                fmt.Println("予期せぬエラーが発生しました")
                break
            } else {
                arr = append(arr, j)
            }   
        }

        // 桁数チェック
        if len(arr) != NumLength {
            fmt.Println("入力された数値の桁数が不正です")
            fmt.Println(q)
            continue
        }

        // 重複チェック
        isDup := func(arr []int) bool {
            var a map[int]int = make(map[int]int)
            for _, v := range arr {
                a[v]++
                if a[v] > 1 {
                    return true
                }
            }
            return false
        }
        if isDup(arr) {
            fmt.Println("入力された値に同じ数値が複数回使われています")
            fmt.Println(q)
            continue
        }

        var hit, blow int
        for pos, val := range arr {
            if ok := isHit(answer, pos, val); ok {
                hit++
            }
            if ok := isBlow(answer, pos, val); ok {
                blow++
            }
        }

        // トライ回数インクリメント
        tryCnt++

        // ヒット&ブロウした数を表示
        fmt.Printf("%d Hit %d Blow\n", hit, blow)

        // 全部ヒットしたら勝ち
        if hit == NumLength {
            fmt.Printf("おめでとうございます!%d回目で全てヒットしました!", tryCnt)
            break
        }

        // 全部ヒットしなかった場合は再度ユーザープロンプトを表示
        fmt.Printf("%s(%d回目)", q, tryCnt)
    }
}

// ヒットしたかどうか
func isHit(arr []int, pos int, val int) bool {
    if arr[pos] == val {
        return true
    } else {
        return false
    }
}

// ブロウしたかどうか
func isBlow(arr []int, pos int, val int) bool {
    inArr := func(num int, arr []int) bool {
        for _, v := range arr {
            if v == num {
                return true
            }
        }
        return false
    }

    // ヒットしているものはブロウとしてカウントしない
    return inArr(val, arr) && !isHit(arr, pos, val)
}

func randGenerate(numLen int) []int {

    // システムが自動発行する数値
    var num int
    // 発番したn桁の乱数配列
    var nums []int

    rand.Seed(time.Now().UnixNano())
    for len(nums) < numLen {
        if len(nums) == 0 {
            nums = append(nums, rand.Intn(10))
        } else {
            num = rand.Intn(10)
            sort.Ints(nums)

            i := sort.Search(len(nums), func(i int) bool { return nums[i] >= num })
            if i < len(nums) && nums[i] == num {
                // numが既にnumsにあればリトライ
                continue
            }
            nums = append(nums, num)
        }
    }

    // 並び替え(シャッフル)
    n := len(nums)
    for i := n - 1; i >= 0; i-- {
        j := rand.Intn(i + 1)
        nums[i], nums[j] = nums[j], nums[i]
    }

    return nums
}

四苦八苦しながらでしたが何とかできました!

という事で、実際に遊んでみたいと思います

f:id:kotakotaking:20160217131246p:plain

#### 14回目でクリア出来ました〜

と、結果の画像間違って消してしまいました↑↑↑

という事で改めてチャレンジ!

f:id:kotakotaking:20160225164609p:plain    

8回目でクリア!!!

   

コードの善し悪しについては置いておいて、

作るものの目的があるとないとでは学習スピードが大きく変わってくると思うので、

是非みなさんもチャレンジしてみてくださいね。

最後に

10名くらいの方と名刺交換を行い、導入事例などを聞いてきました。

実際のサービスでGOを動かしているプロダクトはまだあまり無いようで、

お話しさせていただいた中では、これから導入を検討しているという方が9割程占めていました。

あと、新しいシステムをGOで作るというよりも既存システムをGOリプレイスしたいという声が多かったですね。

導入実績はどんどん増えてくると思うので、これからの可能性を存分に秘めてるなと感じました。