【Go】Hit and Blowを実装してコーディングリハビリ

どもども。最後にコードを書いたのがいつかわからない僕です。

普段からインフラをメイン担当としている都合上コードを書くことは他のメンバーに比べてかなり少なめですが、最近は新規サービスリリースの影響でめっきりインフラ専門人間になっておりました。

気が付けば、コードってどうやって書くんだっけな状態になっていたので、リハビリがてらGoを使ってなにか作ってみようということでHit and Blow実装に取り組んでみましたのでその一部始終をお届けします。

Hit and Blowとは

簡単にHit and Blowについて説明しておきたいと思います。

Hit and Blowとはゲームの一種です。複数桁の定められた数字を予想し的中すれば勝利というルールです。

例えば3桁の数字で「368」という数字が正解だったとしましょう。プレイヤーはまず適当な3桁の数字(例えば「862」とします)を言います。

この場合、数字のみが合っているものが1つ(8) と数字も場所もあっているもの(6)が一つあります。数字のみが合っているものはblow、数字も桁の場所も合っている場合はhitと回答されますのでこの場合は1hit1blowとなります。

次にプレイヤーはこの回答をヒントに次の3桁の数字を選択、また回答を得るという動作をあらかじめ設定されているチャレンジ回数分実行します。

このように3桁の数字を複数回宣言し回答を参考に設定された正解の数字を当てていくというゲームになります。

要件

今回あらかじめ定めた要件は以下の通りです。

– ターミナル上で動作する
– 桁数とチャレンジ回数はプレイヤーが指定できる

必要な処理

いきなりコードを書いていくのではなくまずはhit and blow実現のためにどのような処理が必要か考えていみます。

– プレイヤーから桁数をチャレンジ回数を受け取る処理
– 決められた桁数の正解数字を生成する処理
– プレイヤーから宣言された数字と正解の数字をくらべる処理
– 数字が含まれているのみの場合blowを宣言
– 数字も桁も正しい場合はhitを宣言
– 正解の数字を回答する処理

ざっくりこの辺りが必要になってくるかと思います。では、実際に実装していきましょう。

コーディング

go mod initやtidyなど初期作業は済んでいるものとします。また、必要に応じてパッケージをimportしてください。

まずはmain関数です、ここで正解となる数字の桁数とチャレンジ回数をプレイヤーから受け取れるようにしておきます。

func main() {
	var digit, count int
	fmt.Println("正解数字の桁数を入力してください")
	fmt.Scan(&digit)
	fmt.Println("チャレンジできる回数を入力してください")
	fmt.Scan(&count)
	fmt.Printf("答えの数字は%d桁で、チャレンジ回数は%d回です\n", digit, count)
}

ここで宣言される桁数やチャレンジ回数は今後いろんな処理で使用しますので、引用しやすいように構造体に入れておきたいと思います。

type Codition struct {
	Digit int
	Count int
}

func NewCondition(digit int, count int) Codition {
	return Codition{
		Digit: digit,
		Count: count,
	}
}

構造体を作成する関数はmain関数から呼び出します

func main() {
	var digit, count int
	fmt.Println("正解数字の桁数を入力してください")
	fmt.Scan(&digit)
	fmt.Println("チャレンジできる回数を入力してください")
	fmt.Scan(&count)
	fmt.Printf("答えの数字は%d桁で、チャレンジ回数は%d回です\n", digit, count)

	con := NewCondition(digit, count)
}

これでこの構造体に紐づく形でメソッドを実装していくことができるようになりました。では、正解となる数字を生成するメソッドを実装します。

func (c Codition) createNumber() (int, []string) {
	var correctNumbers []string

	minNumber := int(math.Pow10(c.Digit - 1))
	maxNumber := int(math.Pow10(c.Digit) - 1)

	rand.Seed(time.Now().UnixNano())
	correctNumber := rand.Intn(maxNumber-minNumber) + minNumber
	correctNumberStr := strconv.Itoa(correctNumber)

	for i := 0; i < c.Digit; i++ {
		partNumber := fmt.Sprint(correctNumberStr[i : i+1])
		correctNumbers = append(correctNumbers, partNumber)
	}

	return correctNumber, correctNumbers
}

正解となる数字はランダムに決められる必要があるため"math/rand"を利用します。

また、プレイヤーが決めた桁数の範囲内でランダムな数字を生成する必要があるため、最小値が10のプレイヤーが指定した桁数-1乗、最大値は10のプレイヤーが指定した桁数乗-1で設定しておく必要があります。

今回は10のべき乗を算出する"math.Pow10″を利用することでこれを実現しました。あとは生成された数字を文字列に変換し、一文字ずつスライスに格納しておきます。これはのちに行うプレイヤーの数字と正解の数字を比較するためです。

あとは生成した数字とそれを格納したスライスを返してあげます。メソッドはmain関数から呼び出してあげましょう。

func main() {
	var digit, count int
	fmt.Println("正解数字の桁数を入力してください")
	fmt.Scan(&digit)
	fmt.Println("チャレンジできる回数を入力してください")
	fmt.Scan(&count)
	fmt.Printf("答えの数字は%d桁で、チャレンジ回数は%d回です\n", digit, count)

	con := NewCondition(digit, count)

	resultNum, resultNumbers := con.createNumber()
}

さて、これで正解となる数字を生成することもできました。最後にプレイヤーの数字を受け取り正解の数字と比較する処理を実装しましょう。

func (c Codition) compareNumber(result []string) {
	var answer string
	var answerNumbers []string

	for i := 0; i < c.Count; i++ {

		fmt.Printf("%d回目の回答を入力してください", i+1)
		fmt.Scan(&answer)

   // 入力数字が指定桁数でなかった場合にリトライする
		if len(answer) != c.Digit {
			fmt.Println("指定桁数の数字を入力してください")
			i--
			continue
		}

		for i := 0; i < c.Digit; i++ {
			answerNumber := fmt.Sprint(answer[i : i+1])
			answerNumbers = append(answerNumbers, answerNumber)
		}

		if reflect.DeepEqual(answerNumbers, result) {
			fmt.Printf("%dhit%dblow!!おめでとう、", c.Digit, c.Digit)
			return
		} else {
			blowCount := 0
			hitCount := 0
			for i := 0; i < c.Digit; i++ {
				if answerNumbers[i] == result[i] {
					hitCount++
				}
			}
			for i := 0; i < c.Digit; i++ {
				if slices.Contains(result, answerNumbers[i]) {
					blowCount++
				}
			}
			fmt.Printf("%dhit%dblowです\n", hitCount, blowCount)
			answerNumbers = nil
		}
	}
}

基本的な処理としては、数字を受け取ってスライスに格納し正解数字が格納されているスライスと比較しています。

全て正しい場合はスライスの完全一致といえるので"reflect.DeepEqual"で実装しました。blowかhitの場合はそれぞれのカウンターをあらかじめセットしておき、条件に一致した場合にカウンターを増加させるようにしています。

hitは数字も桁も同じ場合ですが、これはスライスないの同じインデックスの値を比較することで実現できます。blowの場合は入力数字を含むスライスの各インデックスの値が正解スライスに含まれているかを確認すればいいので"golang.org/x/exp/slices"の"slice.Contains"を利用することでこれを実現しています。

あとはこちらのメソッドをmain関数で呼び出してあげればOKです。

func main() {
	var digit, count int
	fmt.Println("正解数字の桁数を入力してください")
	fmt.Scan(&digit)
	fmt.Println("チャレンジできる回数を入力してください")
	fmt.Scan(&count)
	fmt.Printf("答えの数字は%d桁で、チャレンジ回数は%d回です\n", digit, count)

	con := NewCondition(digit, count)

	resultNum, resultNumbers := con.createNumber()
	con.compareNumber(resultNumbers)

	fmt.Printf("正解は%dでした!\n", resultNum)
}

最後に

今回はGoを利用してhit&blowというゲームを作ってみました。個人的にこのゲームめちゃくちゃ好きです!

今回の実装、実はちょっと納得がいっていない部分もあります。例えば、入力する値が数字ではなく文字の場合うまく動きません。

他にもちょこちょこムムムなポイントがありますが、これからちまちまfixしていこうと思います!

ここまで読んでいただきありがとうございました!

Golang

Posted by CY