Golang とgo-glを使用して簡単なBaag-Chalゲームの実装してみました。

# はじめに
Baag-Chal（バーグチャル）はネパールの伝統的なボードゲームで、"Tiger and Goat" ゲームとも呼ばれます。このゲームでは、4匹の虎と20匹の山羊が戦略的に対戦します。本記事では、Golang と go-gl を使用して Baag-Chal ゲームを実装する方法を解説します。

## Baag-Chal（バーグチャル）のルール:

### プレイヤーと駒:

1. プレイヤーは2人で対戦します。
    - 一方のプレイヤーは 虎 (Tiger)、もう一方は 山羊 (Goat) を操作します。
    - ボードは5x5のマス目で構成され、各マスが交差点（ノード）となります。

2. 目的:
    - 虎の目的: 山羊を捕獲し、移動不能にする。
    - 山羊の目的: 虎を移動不能にして捕獲を防ぐ。
3. 初期配置:
    - 4匹の虎はボードの4つの角に配置されます。
    - 山羊は最初はボードに配置されておらず、プレイヤーのターンで順次配置されます。
4. ゲーム進行:
    - 山羊のプレイヤーはターンごとに1匹の山羊を空いている交差点に置きます。すべての山羊（20匹）が配置さ-れると、山羊は移動を開始できます。
虎のプレイヤーは、既に配置された虎を隣接する交差点に移動するか、山羊をジャンプして捕獲します。
5. 捕獲ルール:

    - 虎は隣接する山羊を飛び越えてその後ろの空いている交差点に移動することで捕獲できます。
    - 一度に1匹の山羊しか捕獲できません。
6. 移動ルール:

    - 山羊と虎はどちらも縦・横・斜めの方向に移動できます。ただし、隣接する交差点へのみ移動可能です。
    - 斜めの移動は、ボード上で明確に線が引かれた交差点間でのみ可能です。
7. ゲーム終了条件:

    - 虎が勝つ場合: 5匹以上の山羊を捕獲するか、すべての山羊を移動不能にする。
    - 山羊が勝つ場合: すべての虎が移動できなくなる。


## 必要な環境
- Go 言語（1.20+ 推奨）
- Go-GL ライブラリ
    - github.com/go-gl/gl/v2.1/gl
    - github.com/go-gl/glfw/v3.3/glfw
- OpenGL 2.1 以上

# プロジェクトのセットアップ
```
baagchal
main.go
  game_logic
  render
  events
  assets/
    tiger.png
    goat.png
```

## 必要な依存パッケージのインストール
以下のコマンドで依存パッケージをインストールします：

```
go get github.com/go-gl/gl/v2.1/gl
go get github.com/go-gl/glfw/v3.3/glfw
```

## コード構成
GLFW の初期化、OpenGL コンテキストのセットアップ、ゲームループの管理を行います。
Baag-chal のためにgo-gl を使用して視覚的なゲームを表示するために、まず 5x5 のボードのグリッド線や対角線を描画します。この際、gl.Begin や gl.Vertex2f を用いて線を指定し、線の太さや色は gl.LineWidth と gl.Color3f で設定します。また、駒（虎と山羊）はテクスチャ画像を読み込み、位置情報に基づいて drawPieces 関数で描画されます。さらに、駒のドラッグ操作中には、カーソルの位置に駒を動的に描画する仕組みを drawDraggedPiece 関数で実現しています。

### main.go
``` go
package main

import (
	"log"
	"runtime"

	"github.com/go-gl/gl/v2.1/gl"
	"github.com/go-gl/glfw/v3.3/glfw"
)

const (
	windowWidth  = 1000
	windowHeight = 1000
)

func init() {
	runtime.LockOSThread()
}

func main() {
	if err := glfw.Init(); err != nil {
		log.Fatalln("failed to initialize glfw:", err)
	}
	defer glfw.Terminate()

	window, err := glfw.CreateWindow(windowWidth, windowHeight, "Baag-Chal Board", nil, nil)
	if err != nil {
		log.Fatalln("failed to create window:", err)
	}
	window.MakeContextCurrent()

	if err := gl.Init(); err != nil {
		log.Fatalln("failed to initialize OpenGL:", err)
	}

	initializeGame()

	window.SetMouseButtonCallback(onMouseClick)
	window.SetCursorPosCallback(onMouseMove)
	window.SetKeyCallback(onKeyPress)

	for !window.ShouldClose() {
		gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
		drawBoard()
		drawPieces()
		if draggingPiece {
			drawDraggedPiece()
		}

		window.SwapBuffers()
		glfw.PollEvents()
	}
}
```
### game_logic

``` go
func onGoatPress(boardX, boardY int) {
    // 1) If fewer than 20 goats have been placed, place a new goat
    if placedGoats < 20 {
        if boardState[boardX][boardY] == 0 {
            placeGoat(boardX, boardY)
            // If the goats and tigers to alternate immediately:
            switchTurn()
        }
        return
    }
}

//  handles initiating a drag on a tiger piece
func onTigerPress(boardX, boardY int) {
    if boardState[boardX][boardY] == 2 {
        draggingPiece = true
        selectedPiece = [2]int{boardX, boardY}
        log.Printf("Tiger selected at (%d, %d)", boardX, boardY)
    }
}

//  handles finalizing a move for the piece being dragged.
func onPieceRelease(boardX, boardY int) {
    draggingPiece = false
    from := selectedPiece
    to := [2]int{boardX, boardY}

    if isValidMove(from, to) {
        if canCapture(from, to) {
            captureGoat(from, to)
        } else {
            // Normal move
            boardState[from[0]][from[1]] = 0
            boardState[to[0]][to[1]] = turn
        }
        switchTurn()
    } else {
        log.Printf("Invalid move from (%d, %d) to (%d, %d)", from[0], from[1], to[0], to[1])
    }

    selectedPiece = [2]int{-1, -1}
}

// switchTurn toggles the turn: 1 -> 2, 2 -> 1
func switchTurn() {
    turn = 3 - turn
    log.Printf("Turn switched to %d", turn)
}

// placeGoat puts a goat on the board
func placeGoat(x, y int) {
    boardState[x][y] = 1
    placedGoats++
    log.Printf("Goat placed at (%d, %d). Total placed: %d", x, y, placedGoats)
}
```
こちらは、GLFW を初期化し、OpenGL コンテキストを設定した後に、ゲームループ内でボードと駒を描画しております。また、LoadTexture 関数を使用して山羊と虎のテクスチャをロードし、視覚的にゲームを表現しています。マウス入力やドラッグ操作もコールバックで管理され、ユーザーインタラクションを処理しています。

### render
ボードや駒の描画を管理するために。
``` go
// 5x5 grid plus diagonal lines.
func drawBoard() {
    gl.LineWidth(2.0)
    gl.Color3f(1.0, 1.0, 1.0) // White lines

    // 5 vertical + 5 horizontal lines
    for i := 0; i < 5; i++ {
        // Horizontal
        startX := float32(-0.8)
        startY := float32(-0.8 + 0.4*float32(i))
        endX := float32(0.8)
        endY := startY
        gl.Begin(gl.LINES)
        gl.Vertex2f(startX, startY)
        gl.Vertex2f(endX, endY)
        gl.End()

        // Vertical
        startX = float32(-0.8 + 0.4*float32(i))
        startY = float32(-0.8)
        endX = startX
        endY = float32(0.8)
        gl.Begin(gl.LINES)
        gl.Vertex2f(startX, startY)
        gl.Vertex2f(endX, endY)
        gl.End()
    }

    
    gl.Begin(gl.LINES)
    // Main diagonal
    gl.Vertex2f(-0.8, 0.8)
    gl.Vertex2f(0.8, -0.8)
    // Opposite diagonal
    gl.Vertex2f(-0.8, -0.8)
    gl.Vertex2f(0.8, 0.8)
    // Extra diagonals to center points
    gl.Vertex2f(0.0, 0.8)
    gl.Vertex2f(-0.8, 0.0)
    gl.Vertex2f(0.0, 0.8)
    gl.Vertex2f(0.8, 0.0)
    gl.Vertex2f(0.0, -0.8)
    gl.Vertex2f(-0.8, 0.0)
    gl.Vertex2f(0.0, -0.8)
    gl.Vertex2f(0.8, 0.0)
    gl.End()
}

func drawPieceTex(x, y float32, tex uint32, size float32) {
  gl.BindTexture(gl.TEXTURE_2D, tex)

  half := size * 0.5

  gl.Begin(gl.QUADS)

  // Top-left
  gl.TexCoord2f(0, 0) // Flip vertically
  gl.Vertex2f(x-half, y+half)

  // Top-right
  gl.TexCoord2f(1, 0) // Flip vertically
  gl.Vertex2f(x+half, y+half)

  // Bottom-right
  gl.TexCoord2f(1, 1) // Flip vertically
  gl.Vertex2f(x+half, y-half)

  // Bottom-left
  gl.TexCoord2f(0, 1) // Flip vertically
  gl.Vertex2f(x-half, y-half)
  gl.End()

  gl.BindTexture(gl.TEXTURE_2D, 0)
}

// drawPieces renders goats and tigers on the board.
func drawPieces() {
    for i := 0; i < 5; i++ {
        for j := 0; j < 5; j++ {
            piece := boardState[i][j]
            if piece == 0 {
                continue
            }
            if draggingPiece && selectedPiece == [2]int{i, j} {
                // Skip the piece that is currently being dragged
                continue
            }
            x := boardPosX(i)
            y := boardPosY(j)

            switch piece {
            case 1: // goat
                drawPieceTex(x, y, goatTex, 0.12)
            case 2: // tiger
                drawPieceTex(x, y, tigerTex, 0.15)
            }
        }
    }
}


func drawDraggedPiece() {
    if turn == 1 {
        drawPieceTex(currentDragPos[0], currentDragPos[1], goatTex, 0.12)
    } else {
        drawPieceTex(currentDragPos[0], currentDragPos[1], tigerTex, 0.15)
    }
}

// drawCircle draws a filled circle at (x, y).
func drawCircle(x, y, radius float32, segments int) {
    angleStep := float32(2.0*math.Pi) / float32(segments)
    gl.Begin(gl.POLYGON)
    for i := 0; i < segments; i++ {
        theta := angleStep * float32(i)
        px := x + radius*float32(math.Cos(float64(theta)))
        py := y + radius*float32(math.Sin(float64(theta)))
        gl.Vertex2f(px, py)
    }
    gl.End()
}
```
上記のコードでは、Baag-Chal ゲームのボードと駒を描画しております。ボードは 5x5 のグリッド線と対角線で構成されており、駒（山羊と虎）はそれぞれの位置にテクスチャ付きで描画されております。また、ドラッグ中の駒をマウスカーソルの位置に表示する機能も含まれています。

### event
ユーザーの入力（マウスクリック、ドラッグ、キー押下）を管理します。
``` go
//  handles placing goats, starting drags, and finalizing moves.
func onMouseClick(window *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
    if button != glfw.MouseButtonLeft {
        return
    }
    mouseX, mouseY := window.GetCursorPos()
    boardX, boardY := screenToBoardCoords(mouseX, mouseY)
    if boardX < 0 || boardY < 0 || boardX >= 5 || boardY >= 5 {
        return
    }

    switch action {
    case glfw.Press:
        if turn == 1 {
            // Goat's turn
            onGoatPress(boardX, boardY)
        } else {
            // Tiger's turn
            onTigerPress(boardX, boardY)
        }
    case glfw.Release:
        if draggingPiece {
            onPieceRelease(boardX, boardY)
        }
    }
}

func onMouseMove(window *glfw.Window, xpos, ypos float64) {
    if draggingPiece {
        currentDragPos[0] = float32((xpos / windowWidth) * 2 - 1)
        currentDragPos[1] = float32(1 - (ypos / windowHeight) * 2)
    }
}

func onKeyPress(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
	if action == glfw.Press && key == glfw.KeyR {
		initializeGame()
	}
}
``` 

上記では、マウスのクリックや移動、キーボードの入力に応じてゲームの操作を処理しております。マウスクリックでは駒の選択や移動、配置を管理し、Rキーの入力でゲームをリセットします。また、ドラッグ中の駒はマウスカーソルに追従して描画されます。

画面例

![board.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940772/cc95804d-490b-223e-426d-d4543537125a.png)

![Screenshot 2024-12-24 at 11.17.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940772/47b35b4d-2d66-0fe9-ea06-c7c2d8ee0d9d.png)


## おわりに
ここまで読んでいただきありがとうございます！この記事では、Golang を使った Baag-Chal ゲームの開発について説明しました。GLFW を利用したウィンドウの管理、OpenGL を活用したボードや駒の描画、さらに基本的なゲームロジックの実装までカバーしました。

このプロジェクトは、Golang とグラフィックスプログラミングの基礎を学ぶ良い機会となるはずです。ぜひこの記事を参考にして、ゲームロジックの改良や新たな視覚効果の追加など、独自の工夫を試してみてください。さらに楽しいプロジェクトを作りながら、Golang の可能性を探求してみましょう！笑笑笑

参照
https://en.wikipedia.org/wiki/Bagh-chal
https://github.com/systemi-bikash/baag_chal_gl