Go言語+WindowsTerminalで「ブロック崩し」を作ろうと挑戦したが四苦八苦[termbox-goは未使用]

# 動作画面

↓version 0.9
![PowerShell-2023-09-18-00-45-01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157498/924897a2-6555-6cc9-4fd7-bda33c87d146.gif)

↓version 2.0
![PowerShell-2023-10-22-09-09-58.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157498/aa3e5c97-9c3a-87db-604c-2fcac3e85b8e.gif)


## 説明：とりあえず動けば良いということで
- ボールは、対角斜め（45度・135度・225度・315度）のみ動きます。
- ブロックを全部消しても、ゲームクリアーにならないです。
- ゲームオーバーも無し。

[version 0.9]
- 白棒（バー）は、[a]を押すと左方向、[s]を押すと右方向に動きます。
- スタート画面はないです。コンパイルすると、いきなりゲーム開始。
- キー入力はライブラリの「mattn/go-tty」を使用した。
- 「キー入力」と「画面表記」の非同期はゴルーチンを使用した。

[version 2.0]
- 白棒（バー）は、[←]を押すと左方向、[→]を押すと右方向に動きます。
- スタート画面を追加しました。
- キー入力はWin32の「GetAsyncKeyState」を使用した。
- Win32なので「キー入力」と「画面表記」の非同期は不要となり、ゴルーチンを削除した。


# 車輪の再発明
↓先人が2016年に今回と同じテーマを投稿しており、車輪（タイヤ）の再発名ですが、私個人として「色」と「日本語や絵文字」がGo言語の定番ライブラリ「termbox-go」だと思い通りに使用できなかったので改めて挑戦しました。
 
https://qiita.com/kurehajime/items/a02eaae3f0c17750c07e

https://ja.wikipedia.org/wiki/%E8%BB%8A%E8%BC%AA%E3%81%AE%E5%86%8D%E7%99%BA%E6%98%8E

## 創意工夫した点
- 背景やブロックはANSIエスケープシーケンスを使って着色🟥🟨🟦しています。
- ボールは絵文字の「⚪」です。
- キーボード入力イベントは、Go言語のライブラリ「github.com/mattn/go-tty」を使ってます。
- キーボード入力イベントで検索すると、Go言語の定番TUI(Text User Interface) ライブラリ「termbox-go」がヒットしますが、上記で記載の通り、色や、日本語・絵文字をうまく表示してくれなかったので使用しませんでした。


## 苦労した点
[version 0.9]
- 画面表示はfor文の永久ループ＋ゴルーチンを使ってますが、キーイベントを途中に入れ込むことが出来ず苦労しました。
- 最終的にミリ秒の等間隔で、キーイベントを実行させてむりやり入れ込んでます。
- もっとエレガントな方法があれば、良かったんですが限界でした。

[version 2.0]
- Win32の「GetAsyncKeyState」を使ったら多少エレガントになりました。
- そもそも「Win32」って何？、「GetAsyncKeyState」って何？という話になりますが、長くなるので割愛。


# パソコン環境
- Windows10
- WindowsTerminal 1.19.2831
- go 1.21.3


# 参考にしたサイト（ブロック崩し）
https://qiita.com/kurehajime/items/a02eaae3f0c17750c07e

https://qiita.com/miyaz/items/40360bfa51da17957ad8

https://www.greenowl5.com/gprogram/processing/processing140.html

https://www.youtube.com/watch?v=1dfOCNQyYqg


# 参考にしたサイト（ANSIエスケープシーケンス）
https://www.mm2d.net/main/prog/c/console-02.html


# 参考にしたサイト（キー入力）
https://qiita.com/KEINOS/items/1414e7b13fca97e27f36

https://pineplanter.moo.jp/non-it-salaryman/2017/08/13/go-keyevent-termbox/

https://www.google.com/search?q=golang+GetAsyncKeyState&sca_esv=575511268&sxsrf=AM9HkKnCVlkuioipw8d8BEcEJQc0lM8mLQ%3A1697934934358&ei=Vm40ZezDFdG0oATkrLzIBQ&ved=0ahUKEwjsxvTEtIiCAxVRGogKHWQWD1kQ4dUDCBA&uact=5&oq=golang+GetAsyncKeyState&gs_lp=Egxnd3Mtd2l6LXNlcnAiF2dvbGFuZyBHZXRBc3luY0tleVN0YXRlMgUQABiiBDIFEAAYogQyBRAAGKIESJEyUKQKWNcwcAR4AJABAJgBmQGgAZ4JqgEDMi44uAEDyAEA-AEB-AECwgIHECMYsAMYJ8ICChAAGEcY1gQYsAPCAgQQIxgnwgIEEAAYHsICBhAAGAgYHsICCBAAGAgYHhgKwgIIECEYoAEYwwTiAwQYACBBiAYBkAYE&sclient=gws-wiz-serp

[Githubで参考にしたGetAsyncKeyState](https://gist.github.com/obonyojimmy/61abcbc6022cb7399813db8ac4d1de4d)

# プログラム本文[Version0.9]
157行
約4100文字

```go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/mattn/go-tty"
)

type Bar struct {
	Left int
	Top  int
	W    int
	H    int
}

type Ball struct {
	Left    float64
	Top     float64
	Speed_x float64
	Speed_y float64
}

type Block struct {
	Left   int
	Top    int
	Color  string
	Exists int
}

const tick_time_str time.Duration = 200
const tick_time_tty time.Duration = 10
const white string = "\033[48;2;255;255;255m　\033[0m" //⬜
const red string = "\033[48;2;255;0;0m　\033[0m"       //🟥
const blue string = "\033[48;2;0;0;255m　\033[0m"      //🟦
const yellow string = "\033[48;2;255;255;0m　\033[0m"  //🟨

func main() {
	fmt.Println("\033[2J") //画面全体を消去

	ball := Ball{
		Left: 15, Top: 5, Speed_x: 1, Speed_y: 1,
	}

	bar := Bar{
		Left: 7, Top: 27, W: 5, H: 1,
	}

	block_data := []Block{
		{2, 2, red, 1}, {4, 2, red, 1}, {6, 2, red, 1}, {8, 2, red, 1}, {10, 2, red, 1}, {12, 2, red, 1}, {14, 2, red, 1},
		{3, 3, blue, 1}, {4, 3, blue, 1}, {5, 3, blue, 1}, {6, 3, blue, 1}, {8, 3, blue, 1}, {9, 3, blue, 1}, {10, 3, blue, 1}, {11, 3, blue, 1}, {13, 3, blue, 1},
		{2, 4, yellow, 1}, {3, 4, yellow, 1}, {4, 4, yellow, 1}, {5, 4, yellow, 1}, {6, 4, yellow, 1}, {7, 4, yellow, 1}, {8, 4, yellow, 1}, {9, 4, yellow, 1}, {10, 4, yellow, 1},
	}

	chan_Print_str := make(chan string)
	go go_Print_str(chan_Print_str)
	defer close(chan_Print_str)

	tty, err := tty.Open()
	if err != nil {
		log.Fatal(err)
	}
	defer tty.Close()

	go func() {
		for range time.Tick(tick_time_str * time.Millisecond) {
			chan_Print_str <- creat_str(ball, bar, block_data)
			ball, block_data = ballHitCheck(ball, bar, block_data)
		}
	}()

	for range time.Tick(tick_time_tty * time.Millisecond) {
		r, err := tty.ReadRune()
		if err != nil {
			log.Fatal(err)
		}

		//aが左に、sが右に
		if string(r) == "s" { //r=115
			bar.Left++
		} else if string(r) == "a" { //r=97
			bar.Left--
		}

	} //for range time.Tick(tick_time_tty * time.Millisecond) {
}

func go_Print_str(chan_Print_str chan string) {
	fmt.Println("\033[2J\033[0;0H")
	for {
		//ゲーム画面を描画する
		fmt.Println(<-chan_Print_str)
		time.Sleep(tick_time_str * time.Millisecond)
	}
}

func creat_str(ball Ball, bar Bar, block []Block) string {

	str := "\033[0;0H" //カーソルを原点に戻す
	var pixel [22][32]string

	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			if float64(pixel_left) <= ball.Left && ball.Left < float64(pixel_left+1) && float64(pixel_top) <= ball.Top && ball.Top < float64(pixel_top+1) {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m⚪\033[0m"
			} else if bar.Left < pixel_left && bar.Left >= pixel_left-bar.W && bar.Top < pixel_top && bar.Top >= pixel_top-bar.H {
				pixel[pixel_left][pixel_top] = white
			} else {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m　\033[0m"
			}

			for i, _ := range block {
				if pixel_top == block[i].Top && pixel_left == block[i].Left && block[i].Exists == 1 {
					pixel[pixel_left][pixel_top] = block[i].Color
				}
			}

		}
	}

	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			str += pixel[pixel_left][pixel_top]
		}
		str += "\n"
	}

	return str
}

func ballHitCheck(ball Ball, bar Bar, block []Block) (Ball, []Block) {
	ball.Left += ball.Speed_x
	ball.Top += ball.Speed_y
	//左右の壁と接触判定
	if ball.Left < 1 || ball.Left > 20 {
		ball.Speed_x = -ball.Speed_x
	}
	//上下の壁と接触判定
	if ball.Top < 1 || ball.Top > 30 {
		ball.Speed_y = -ball.Speed_y
	}
	//棒とボールの接触判定
	if ball.Top > float64(bar.Top) && ball.Top <= float64(bar.Top+bar.H) && ball.Left > float64(bar.Left) && ball.Left <= float64(bar.Left+bar.W) {
		ball.Speed_y = -ball.Speed_y
	}

	//ボールとブロックの接触判定
	for i, _ := range block {
		if ball.Top > float64(block[i].Top) && ball.Top <= float64(block[i].Top+1) && ball.Left > float64(block[i].Left) && ball.Left <= float64(block[i].Left+1) {
			ball.Speed_y = -ball.Speed_y
			block[i].Exists = 0
		}
	}

	return ball, block
}


```

# プログラム本文[Version2.0]
160行
約4700文字

```golang
package main

import (
	"fmt"
	"time"

	"golang.org/x/sys/windows"
)

type Bar struct {
	Left int
	Top  int
	W    int
	H    int
}

type Ball struct {
	Left    float64
	Top     float64
	Speed_x float64
	Speed_y float64
}

type Block struct {
	Left   int
	Top    int
	Color  string
	Exists int
}

var game_start = 0

const game_time = 100
const white string = "\033[48;2;255;255;255m　\033[0m" //⬜
const red string = "\033[48;2;255;0;0m　\033[0m"       //🟥
const blue string = "\033[48;2;0;0;255m　\033[0m"      //🟦
const yellow string = "\033[48;2;255;255;0m　\033[0m"  //🟨
const start_screen string = `*--------------------*
|Go言語でブロック崩し|
*--------------------*

*開始するには「スペース」ボタンを押してください*`

var (
	moduser32            = windows.NewLazyDLL("user32.dll")
	procGetAsyncKeyState = moduser32.NewProc("GetAsyncKeyState")
)

func main() {
	//fmt.Println("\033[2J")    //画面全体を消去
	fmt.Println("\033[2J\033[0;0H")
	fmt.Println(start_screen) //画面全体を消去

	ball := Ball{
		Left: 15, Top: 5, Speed_x: 1, Speed_y: 1,
	}

	bar := Bar{
		Left: 7, Top: 27, W: 5, H: 1,
	}

	block_data := []Block{
		{2, 2, red, 1}, {4, 2, red, 1}, {6, 2, red, 1}, {8, 2, red, 1}, {10, 2, red, 1}, {12, 2, red, 1}, {14, 2, red, 1}, {16, 2, red, 1}, {18, 2, red, 1},
		{3, 3, blue, 1}, {4, 3, blue, 1}, {5, 3, blue, 1}, {6, 3, blue, 1}, {8, 3, blue, 1}, {9, 3, blue, 1}, {10, 3, blue, 1}, {11, 3, blue, 1}, {13, 3, blue, 1}, {14, 3, blue, 1}, {15, 3, blue, 1},
		{2, 4, yellow, 1}, {3, 4, yellow, 1}, {4, 4, yellow, 1}, {5, 4, yellow, 1}, {6, 4, yellow, 1}, {7, 4, yellow, 1}, {8, 4, yellow, 1}, {9, 4, yellow, 1}, {10, 4, yellow, 1}, {11, 4, yellow, 1}, {12, 4, yellow, 1}, {13, 4, yellow, 1}, {14, 4, yellow, 1},
	}

	for {
		asynch32, _, _ := procGetAsyncKeyState.Call(uintptr(32)) //space
		if asynch32 != 0 {
			game_start = 1
			break
		}
	}

	if game_start == 1 {
		fmt.Println("\033[2J\033[0;0H")
		for {
			//ゲーム画面を描画する
			ball, block_data = ballHitCheck(ball, bar, block_data)
			asynch37, _, _ := procGetAsyncKeyState.Call(uintptr(37)) //←
			if asynch37 != 0 {
				bar.Left--
			}

			asynch39, _, _ := procGetAsyncKeyState.Call(uintptr(39)) //→
			if asynch39 != 0 {
				bar.Left++
			}
			fmt.Println("asynch37", asynch37, asynch37&0x1)
			fmt.Println("asynch39", asynch39, asynch39&0x1)

			fmt.Println(creat_str(ball, bar, block_data))
			time.Sleep(game_time * time.Millisecond)
		}
	}
}

func creat_str(ball Ball, bar Bar, block []Block) string {
	str := "\033[0;0H" //カーソルを原点に戻す
	var pixel [22][32]string
	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		//玉と空間の位置情報をデータに格納
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			if float64(pixel_left) <= ball.Left && ball.Left < float64(pixel_left+1) && float64(pixel_top) <= ball.Top && ball.Top < float64(pixel_top+1) {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m⚪\033[0m"
			} else if bar.Left < pixel_left && bar.Left >= pixel_left-bar.W && bar.Top < pixel_top && bar.Top >= pixel_top-bar.H {
				pixel[pixel_left][pixel_top] = white
			} else {
				pixel[pixel_left][pixel_top] = "\033[48;2;100;100;100m　\033[0m"
			}

			//ブロックの位置情報をデータ格納
			for i, _ := range block {
				if pixel_top == block[i].Top && pixel_left == block[i].Left && block[i].Exists == 1 {
					pixel[pixel_left][pixel_top] = block[i].Color
				}
			}
		}
	}

	//strデータに、全位置情報を格納
	for pixel_top := 0; pixel_top < 32; pixel_top++ {
		for pixel_left := 0; pixel_left < 22; pixel_left++ {
			str += pixel[pixel_left][pixel_top]
		}
		str += "\n"
	}

	return str
}

// 接触判定
func ballHitCheck(ball Ball, bar Bar, block []Block) (Ball, []Block) {
	ball.Left += ball.Speed_x
	ball.Top += ball.Speed_y
	//左右の壁と接触判定
	if ball.Left < 1 || ball.Left > 20 {
		ball.Speed_x = -ball.Speed_x
	}
	//上下の壁と接触判定
	if ball.Top < 1 || ball.Top > 30 {
		ball.Speed_y = -ball.Speed_y
	}
	//棒とボールの接触判定
	if ball.Top > float64(bar.Top) && ball.Top <= float64(bar.Top+bar.H) && ball.Left > float64(bar.Left) && ball.Left <= float64(bar.Left+bar.W) {
		ball.Speed_y = -ball.Speed_y
	}

	//ボールとブロックの接触判定
	for i, _ := range block {
		if block[i].Exists == 1 && ball.Top > float64(block[i].Top) && ball.Top <= float64(block[i].Top+1) && ball.Left > float64(block[i].Left) && ball.Left <= float64(block[i].Left+1) {
			ball.Speed_x = -ball.Speed_x
			ball.Speed_y = -ball.Speed_y
			block[i].Exists = 0
		}
	}

	return ball, block
}
```