Closed11

GitHub CopilotとペアプロTDDでローグライクRPGを作る本メモ

ぱんだぱんだ

4章ダンジョンの作成

ダンジョンの作成方法を聞いてみる。

prompt
ローグライクRPGのダンジョンを自動生成する方法を教えて
回答
ローグライクRPGのダンジョン自動生成には様々なアルゴリズムがありますが、一般的な手法の一つに「部屋と通路」のアプローチがあります。以下にその基本的な手順を説明します。

- マップを初期化:最初に、全てのタイルを壁に設定します。

- 部屋を生成:ランダムな位置、大きさで部屋を生成します。部屋が他の部屋と重ならないことを確認します。

- 部屋をつなげる通路を生成:部屋と部屋をつなげる通路を生成します。これには、例えば最短経路探索アルゴリズムなどが使えます。

-(オプション)追加の特徴を生成:階段、トラップ、宝箱などの特徴をランダムな位置に生成します。

-(オプション)モンスターやアイテムを配置:部屋や通路にモンスターやアイテムをランダムに配置します。

この手順は基本的なもので、より複雑なダンジョンを生成するためには、迷路生成アルゴリズム、パーリンノイズ、セルオートマトンなどの他のアルゴリズムを組み合わせることもあります。

また、ダンジョン生成の結果はゲームのバランスに大きく影響しますので、生成されたダンジョンがプレイヤーにとって適切な難易度となるように、パラメータの調整も重要です。

マップチップの定義

prompt
「ローグライク RPG のダンジョンに使用するマップチップの enum を作ってください。壁、部屋、通路、ドア、登り階段、降り階段を含めてくだ さい」
dungeon/maptile.go
type MapTile int

const (
    Wall MapTile = iota
    Room
    Corridor
    Door
    UpStairs
    DownStairs
)

マップの初期化

マップ初期化処理のTODOリストは以下

  • 指定サイズのマップ配列が生成されること
  • マップ配列が壁で埋められていること

テストを書く

dungeon/mapgenerator_test.go
package dungeon_test

import (
	"roguelike-tdd-go/dungeon"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestMapGenerator(t *testing.T) {
	testcases := map[string]struct {
		width  int
		height int
	}{
		"20x10: 指定サイズのマップ配列が生成されること": {
			width:  20,
			height: 10,
		},
		"30x15: 指定サイズのマップ配列が生成されること": {
			width:  30,
			height: 15,
		},
	}

	for name, tt := range testcases {
		t.Run(name, func(t *testing.T) {
			// Act
			sut := dungeon.NewMapGenerator(tt.width, tt.height)

			// Assert
			if len(sut.Map) != tt.height {
				t.Errorf("Map height: got %v, want %v", len(sut.Map), tt.height)
			}

			for _, row := range sut.Map {
				if len(row) != tt.width {
					t.Errorf("Map width: got %v, want %v", len(row), tt.width)
				}
			}
		})
	}
}

ポイントは以下

  • なるべくマジックナンバーなどは避け、Copilotが正しいコードを推測しやすいようにヒントを与えるイメージ
  • 入力と出力が明確になるようにテスト関数名なども注意する
  • 自然言語に近いアサーション
  • テストが書けたらテストファイルは開きっぱなしにしておく

次にマップのタイルが全てWallで初期化されている必要があるのでテストを追加する。

dungeon/mapgenerator_test.go

func TestMapGenerator_InitTile(t *testing.T) {
	// Arrange
	width, height := 20, 10
	expected := make([][]dungeon.MapTile, height)
	for i := range expected {
		row := make([]dungeon.MapTile, width)
		for j := range row {
			row[j] = dungeon.Wall
		}
		expected[i] = row
	}

	// Act
	sut := dungeon.NewMapGenerator(width, height)

	// Assert
	assert.Exactly(t, expected, sut.Map)
}

このテストをもとに書いてもらった実装が以下

dungeon/mapgenerator.go
package dungeon

type MapGenerator struct {
	Map [][]MapTile
}

// NewMapGenerator は、指定されたサイズでマップタイルは全てWallで初期化されたMapGeneratorを生成します。
func NewMapGenerator(width, height int) *MapGenerator {
	mg := &MapGenerator{
		Map: make([][]MapTile, height),
	}
	for y := range mg.Map {
		mg.Map[y] = make([]MapTile, width)
		for x := range mg.Map[y] {
			mg.Map[y][x] = Wall
		}
	}
	return mg
}
  • Wallで初期化する処理を書いてくれなかったのでコメントを追加した
  • テストは全てパスしたのでとりあえずok

部屋の生成

部屋の生成処理のTODOリストは以下

  • Room構造体の定義
  • 指定した区画内にランダムな位置・サイズの部屋を生成するメソッド
    • 指定した区画内に収まる位置・サイズの部屋を返すこと
    • 部屋の最小サイズを指定できること
    • 指定した区画と部屋のパディングを空けられること
    • 部屋の位置・サイズがランダムであること
  • マップを9区画に区切って9つの部屋を生成するメソッド
    • 部屋が9つ生成されること
    • 各部屋は9つの区画内に生成されること
  • 部屋をマップ配列に書き込めること

Room構造体を作っていく

prompt
dungeonパッケージ配下に部屋を定義する構造体を定義してください。
dungeon/room.go
package dungeon

type Room struct {
    X      int // Room's left top x-coordinate
    Y      int // Room's left top y-coordinate
    Width  int // Width of the room
    Height int // Height of the room
}

ファクトリ関数も作って欲しかったので

prompt
ファクトリ関数も作成してください。
dungeon/room.go
package dungeon

// NewRoom は、指定された位置とサイズで新しいRoomを生成します。
func NewRoom(x, y, width, height int) *Room {
    return &Room{
        X:      x,
        Y:      y,
        Width:  width,
        Height: height,
    }
}

ここで最初に定義したMapTile定数のRoomと構造体のRoomの命名が衝突してコンパイルエラーとなってしまいました。

prompt
MapTile定数のRoomと構造体のRoomが名前衝突してしまいました。どちらかの命名を修正してください。
回答
確かに、MapTile の定数と Room 構造体が名前衝突を起こしています。この問題を解決するために、MapTile の Room 定数の名前を RoomTile に変更します。

以下に修正後のコードを示します。

package dungeon

type MapTile int

const (
    Undefined MapTile = iota
    Wall
    RoomTile
    Passage
    Door
    UpStairs
    DownStairs
)

これで、RoomTile はマップチップを表し、Room 構造体は部屋の位置とサイズを表すようになりました。
ぱんだぱんだ

指定した区画内に収まる位置・サイズの部屋を返すことを検証するテストを書く

dungeon/room_test.go
package dungeon_test

import (
	"fmt"
	"roguelike-tdd-go/dungeon"
	"testing"

	"github.com/stretchr/testify/assert"
)

type testCase struct {
	left   int
	top    int
	right  int
	bottom int
}

func (tt *testCase) name(s string) string {
	return fmt.Sprintf("%s: left=%d, top=%d, right=%d, bottom=%d", s, tt.left, tt.top, tt.right, tt.bottom)
}

func TestNewRoomInBounds(t *testing.T) {
	testcases := map[string]testCase{
		"部屋が指定区画内に作られること": {
			left:   0,
			top:    0,
			right:  5,
			bottom: 5,
		},
		"部屋が指定区画内に作られること2": {
			left:   20,
			top:    10,
			right:  39,
			bottom: 19,
		},
	}

	for name, tt := range testcases {
		t.Run(tt.name(name), func(t *testing.T) {
			// Arrange
			roomMaxWidth := tt.right - tt.left + 1
			roomMaxHeight := tt.bottom - tt.top + 1

			// Act
			sut := dungeon.NewRoomInBounds(tt.left, tt.top, tt.right, tt.bottom)

			// Assert
			assert.True(t, tt.left <= sut.X && sut.X <= tt.right)
			assert.True(t, tt.top <= sut.Y && sut.Y <= tt.bottom)
			assert.True(t, sut.Width <= roomMaxWidth)
			assert.True(t, sut.Height <= roomMaxHeight)
		})
	}
}

ポイント

  • 実際のコードは疑似乱数を使いランダム性を持たせる必要がある
  • が、テストコードではそれは表現されておらず、匂わせることが大事
  • ランダム性のあるテストなので本書では100回くらい繰り返し実行をしていた
  • こういうときはPBTの使いどきかもしれない

生成されたコード

dungeon/room.go
package dungeon

import (
	"math/rand"
)

type Room struct {
	X      int // Room's left top x-coordinate
	Y      int // Room's left top y-coordinate
	Width  int // Width of the room
	Height int // Height of the room
}

// NewRoom は、指定された位置とサイズで新しいRoomを生成します。
func NewRoom(x, y, width, height int) *Room {
	return &Room{
		X:      x,
		Y:      y,
		Width:  width,
		Height: height,
	}
}

// 指定された区画内にランダムな位置とサイズで新しいRoomを生成します。
// Roomの最小サイズは3x3です。
// rand.Intn(n)のnは1以上の整数が指定されるようにし、関数内でpanicは起きないものとする。
func NewRoomInBounds(left, top, right, bottom int) *Room {
	roomMaxWidth := right - left + 1
	roomMaxHeight := bottom - top + 1

	// Roomの最小サイズは3x3
	width := 3 + rand.Intn(roomMaxWidth-3)
	height := 3 + rand.Intn(roomMaxHeight-3)

	x := left + rand.Intn(roomMaxWidth-width)
	y := top + rand.Intn(roomMaxHeight-height)

	return &Room{
		X:      x,
		Y:      y,
		Width:  width,
		Height: height,
	}
}

コメント書いて頑張ったりしたけど正しいコードにならない。最小サイズが3x3で固定なのは本書もそうなのでいいけど、生成される乱数の範囲が微妙に違いそう。

と思ったけど正しそう。疑似乱数生成のseedの初期化だけ漏れているので追記。外部からseedは指定できたほうがテストしやすいのかもしれないがどっちがいいかまだわからないので一旦関数内で初期化。

dungeon/room.go
// 指定された区画内にランダムな位置とサイズで新しいRoomを生成します。
// Roomの最小サイズは3x3です。
func NewRoomInBounds(left, top, right, bottom int) *Room {
	roomMaxWidth := right - left + 1
	roomMaxHeight := bottom - top + 1

	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	// Roomの最小サイズは3x3
	width := 3 + r.Intn(roomMaxWidth-3)
	height := 3 + r.Intn(roomMaxHeight-3)

	x := left + r.Intn(roomMaxWidth-width)
	y := top + r.Intn(roomMaxHeight-height)

	return &Room{
		X:      x,
		Y:      y,
		Width:  width,
		Height: height,
	}
}

と思ったけどなんか違う気がしたので以下のように修正。Copilotのみ編集の縛り破ってしまった。

dungeon/room.go
// 指定された区画内にランダムな位置とサイズで新しいRoomを生成します。
// Roomの最小サイズは3x3です。
func NewRoomInBounds(left, top, right, bottom int) *Room {
	roomMaxWidth := right - left + 1
	roomMaxHeight := bottom - top + 1

	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	const minimumRoomSize = 3

	var (
		width  = minimumRoomSize
		height = minimumRoomSize
		x      = left
		y      = top
	)

	// Roomの最小サイズは3x3
	if roomMaxWidth > minimumRoomSize {
		width = minimumRoomSize + r.Intn(roomMaxWidth-minimumRoomSize)
	}
	if roomMaxHeight > minimumRoomSize {
		height = minimumRoomSize + r.Intn(roomMaxHeight-minimumRoomSize)
	}

	if right-width+1-left > 0 {
		x = left + r.Intn(right-width+1-left)
	}
	if bottom-height+1-top > 0 {
		y = top + r.Intn(bottom-height+1-top)
	}

	return &Room{
		X:      x,
		Y:      y,
		Width:  width,
		Height: height,
	}
}

一応テストは通るので先に進む。
こういう境界値が重要なやつ難しい。。

ぱんだぱんだ

部屋の最小サイズの指定

テストを修正

dungeon/room_test.go
func TestNewRoomInBounds(t *testing.T) {
	testcases := map[string]testCase{
		"部屋が指定区画内に作られること": {
			left:        0,
			top:         0,
			right:       5,
			bottom:      5,
			minRoomSize: 1,
		},
		"部屋が指定区画内に作られること2": {
			left:        20,
			top:         10,
			right:       39,
			bottom:      19,
			minRoomSize: 5,
		},
	}

	for name, tt := range testcases {
		t.Run(tt.name(name), func(t *testing.T) {
			// Arrange
			roomMaxWidth := tt.right - tt.left + 1
			roomMaxHeight := tt.bottom - tt.top + 1

			// Act
			sut := dungeon.NewRoomInBounds(tt.left, tt.top, tt.right, tt.bottom, tt.minRoomSize)

			// Assert
			assert.True(t, tt.left <= sut.X && sut.X <= tt.right)
			assert.True(t, tt.top <= sut.Y && sut.Y <= tt.bottom)
			assert.True(t, tt.minRoomSize <= sut.Width && sut.Width <= roomMaxWidth)
			assert.True(t, tt.minRoomSize <= sut.Height && sut.Height <= roomMaxHeight)
		})
	}
}

実装も作り直してみる

dungeon/room.go
// 指定された区画内にランダムな位置とサイズで新しいRoomを生成します。
func NewRoomInBounds(left, top, right, bottom, minRoomSize int) *Room {
	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	roomMaxWidth := right - left + 1
	roomMaxHeight := bottom - top + 1

	width := r.Intn(roomMaxWidth-minRoomSize) + minRoomSize
	height := r.Intn(roomMaxHeight-minRoomSize) + minRoomSize

	x := r.Intn(right-width+1-left) + left
	y := r.Intn(bottom-height+1-top) + top

	return NewRoom(x, y, width, height)
}
  • 乱数の初期化をしてくれない
  • x, yの生成がなんか違いそうなの提案してくる
  • r.Intn()に指定する値が0以下だとpanicするが、なんか怪しい匂いがする
  • panicしそうな値の組み合わせのテストを作って安心したくなる
  • その際はpanicさせるよりもerrorを返すようにしたほうがテストしやすいかも
  • そうなると関数内でバリデーションロジックを書く必要がある
ぱんだぱんだ

指定した区画と部屋のパディングを空けられるようにする

マップの外周を壁にするためと通路を通すことを考え、パディングを指定できるようにする。

テスト修正

dungeon/room_test.go
func TestNewRoomInBounds(t *testing.T) {
	testcases := map[string]testCase{
		"部屋が指定区画内に作られること": {
			left:        0,
			top:         0,
			right:       5,
			bottom:      5,
			minRoomSize: 1,
			padding:     0,
		},
		"部屋が指定区画内に作られること2": {
			left:        20,
			top:         10,
			right:       39,
			bottom:      19,
			minRoomSize: 5,
			padding:     2,
		},
	}

	for name, tt := range testcases {
		t.Run(tt.name(name), func(t *testing.T) {
			// Arrange
			roomMaxWidth := tt.right - tt.left + 1 - tt.padding*2
			roomMaxHeight := tt.bottom - tt.top + 1 - tt.padding*2

			roomLeft := tt.left + tt.padding
			roomTop := tt.top + tt.padding
			roomRight := tt.right - tt.padding
			roomBottom := tt.bottom - tt.padding

			// Act
			sut := dungeon.NewRoomInBounds(
				tt.left,
				tt.top,
				tt.right,
				tt.bottom,
				func(opt *dungeon.Option) {
					opt.MinRoomSize = tt.minRoomSize
					opt.Padding = tt.padding
				},
			)

			// Assert
			assert.True(t, roomLeft <= sut.X && sut.X <= roomRight)
			assert.True(t, roomTop <= sut.Y && sut.Y <= roomBottom)
			assert.True(t, tt.minRoomSize <= sut.Width && sut.Width <= roomMaxWidth)
			assert.True(t, tt.minRoomSize <= sut.Height && sut.Height <= roomMaxHeight)
		})
	}
}

ついでに最小roomサイズとパディングの指定はオプションとするように変更してみた。実装の作り直しは以下。

dungeon/room.go
type Option struct {
	MinRoomSize int
	Padding     int
}

type Options func(*Option)

// 指定された区画内にランダムな位置とサイズで新しいRoomを生成します。
// minRoomSizeとpaddingはデフォルト値を持ち、省略可能
// minRooSizeとpaddingはfunctionally optionとして扱う
func NewRoomInBounds(left, top, right, bottom int, options ...Options) *Room {
	opt := &Option{
		MinRoomSize: 1,
		Padding:     0,
	}
	for _, o := range options {
		o(opt)
	}

	roomMaxWidth := right - left + 1 - opt.Padding*2
	roomMaxHeight := bottom - top + 1 - opt.Padding*2

	roomLeft := left + opt.Padding
	roomTop := top + opt.Padding
	// roomRight := right - opt.Padding
	// roomBottom := bottom - opt.Padding

	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	roomWidth := r.Intn(roomMaxWidth-opt.MinRoomSize) + opt.MinRoomSize
	roomHeight := r.Intn(roomMaxHeight-opt.MinRoomSize) + opt.MinRoomSize

	roomX := r.Intn(roomMaxWidth-roomWidth) + roomLeft
	roomY := r.Intn(roomMaxHeight-roomHeight) + roomTop

	return NewRoom(roomX, roomY, roomWidth, roomHeight)
}
  • widthとheightを+1しないと最大値を生成できない
  • 相変わらずxとyのロジックが合ってるのかわからない
  • 一応テストは通るから進んじゃう
ぱんだぱんだ

部屋の位置、サイズがランダムであること

100回Room作成し、RoomのX座標を保存しておく。100回終わったあとに保存していた件数が1件よりも多いことを確認して、ランダム生成されていることを確認する。

dungeon/room_test.go
func TestNewRoomInBounds_RoomSizeRandom(t *testing.T) {
	left, top, right, bottom := 0, 0, 19, 9
	xDistribution := make(map[int]struct{})

	for range 100 {
		sut := dungeon.NewRoomInBounds(left, top, right, bottom)
		xDistribution[sut.X] = struct{}{}
	}

	assert.True(t, len(xDistribution) > 1)
}
  • ランダム性のあるテストだとこういうことしなきゃいけない
  • 難しい
  • PBTでなんか疑似乱数のテストの話あったようななかったような
ぱんだぱんだ

指定した最大サイズの部屋が作られること

テスト

dungeon/room_test.go
func TestNewRoomInBounds_MakeMaxSizeRoom(t *testing.T) {
	// Arrange 固定値
	left, top, right, bottom := 0, 0, 4, 4
	minRoomSize := 2
	padding := 1

	// 生成Roomは9パターン
	expected := map[int]dungeon.Room{
		1122: {X: 1, Y: 1, Width: 2, Height: 2},
		2122: {X: 2, Y: 1, Width: 2, Height: 2},
		1222: {X: 1, Y: 2, Width: 2, Height: 2},
		2222: {X: 2, Y: 2, Width: 2, Height: 2},
		1132: {X: 1, Y: 1, Width: 3, Height: 2},
		1232: {X: 1, Y: 2, Width: 3, Height: 2},
		1123: {X: 1, Y: 1, Width: 2, Height: 3},
		2123: {X: 2, Y: 1, Width: 2, Height: 3},
		1133: {X: 1, Y: 1, Width: 3, Height: 3},
	}

	roomDistribution := make(map[int]dungeon.Room)

	// Act
	for range 100 {
		sut := dungeon.NewRoomInBounds(left, top, right, bottom, func(opt *dungeon.Option) {
			opt.MinRoomSize = minRoomSize
			opt.Padding = padding
		})
		roomDistribution[sut.X*1000+sut.Y*100+sut.Width*10+sut.Height] = *sut
	}

	// Assert
	assert.Equal(t, expected, roomDistribution)
}
  • SetがGoにはないのでMapで表現
  • 100回実行して生成ルームパターンを全部洗い出す
  • それが想定通りの9パターンか検証するテスト
  • しかし、これは失敗する

これまでの実装もやはりおかしかったのでもろもろ修正

dungeon/room.go
func NewRoomInBounds(left, top, right, bottom int, options ...Options) *Room {
	opt := &Option{
		MinRoomSize: 1,
		Padding:     0,
	}
	for _, o := range options {
		o(opt)
	}

	roomMaxWidth := right - left + 1 - opt.Padding*2
	roomMaxHeight := bottom - top + 1 - opt.Padding*2

	roomLeft := left + opt.Padding
	roomTop := top + opt.Padding
	roomRight := right - opt.Padding
	roomBottom := bottom - opt.Padding

	r := rand.New(rand.NewSource(time.Now().UnixNano()))

	roomWidth := r.Intn(roomMaxWidth+1-opt.MinRoomSize) + opt.MinRoomSize
	roomHeight := r.Intn(roomMaxHeight+1-opt.MinRoomSize) + opt.MinRoomSize

	roomX, roomY := roomLeft, roomTop
	if roomRight-roomWidth+2-roomLeft > 0 {
		roomX = r.Intn(roomRight-roomWidth+2-roomLeft) + roomLeft
	}
	if roomBottom-roomHeight+2-roomTop > 0 {
		roomY = r.Intn(roomBottom-roomHeight+2-roomTop) + roomTop
	}

	return NewRoom(roomX, roomY, roomWidth, roomHeight)
}

乱数生成するときに0以下を指定するとpanicするのでガード節が必要。ここ微妙なのでなんとかしたい。validationロジックを最低でもいれたほうがいいのかもしれない。

ぱんだぱんだ

部屋の座標計算プロパティの追加

テスト

dungeon/room_test.go
func TestRight(t *testing.T) {
	// Arrange
	testcases := map[string]struct {
		x     int
		width int
		right int
	}{
		"when x: 0 width: 10, expected right: 9":  {x: 0, width: 10, right: 9},
		"when x: 5 width: 15, expected right: 19": {x: 5, width: 15, right: 19},
	}

	for name, tt := range testcases {
		t.Run(name, func(t *testing.T) {
			// Act
			sut := dungeon.NewRoom(tt.x, 0, tt.width, 0)

			// Assert
			assert.Equal(t, tt.right, sut.Right())
		})
	}
}

提案された実装

dungeon/room.go
func (r *Room) Right() int {
	return r.X + r.Width - 1
}

テストもパスした。こういう入力と出力がわかりやすいのはテストも書きやすいし、実装も提案されやすそう。

ぱんだぱんだ

部屋をマップ配列に書き込めることを検証

本書ではroom構造体の関数として実装していたが、MapGeneratorがマップ配列を状態として保持しているので状態を書き換えるような処理はMapGenerator自身に持たせることにした。

テストは以下

dungeon/mapgenerator_test.go
func TestMapGenerator_Write(t *testing.T) {
	// Arrange
	mapWidth, mapHeight := 4, 2
	roomX, roomY, roomWidth, roomHeight := 1, 0, 2, 1

	expected := [][]dungeon.MapTile{
		{dungeon.Wall, dungeon.RoomTile, dungeon.RoomTile, dungeon.Wall},
		{dungeon.Wall, dungeon.Wall, dungeon.Wall, dungeon.Wall},
	}

	room := dungeon.NewRoom(roomX, roomY, roomWidth, roomHeight)

	// Act
	sut := dungeon.NewMapGenerator(mapWidth, mapHeight)
	sut.Write(room)

	// Assert
	assert.Exactly(t, expected, sut.Map)
}

提案された実装

mapgenerator.go
func (mg *MapGenerator) Write(room *Room) {
	for y := room.Y; y < room.Y+room.Height; y++ {
		for x := room.X; x < room.X+room.Width; x++ {
			mg.Map[y][x] = RoomTile
		}
	}
}

テストも通った

ぱんだぱんだ

部屋が9つ生成されることの検証

テスト

dungeon/room_test.go
func TestCreateRooms(t *testing.T) {
	// Arrange
	minRooSize := 1
	padding := 0

	testcases := map[string]struct {
		width  int
		height int
	}{
		"15 x12のマップが9つ作成されること":  {width: 15, height: 12},
		"18 x 15のマップが9つ作成されること": {width: 18, height: 15},
	}

	for name, tt := range testcases {
		t.Run(name, func(t *testing.T) {
			// Act
			sut := dungeon.CreateRooms(tt.width, tt.height, minRooSize, padding)

			// Assert
			assert.Len(t, sut, 9)
		})
	}
}

提案された実装(うまく生成されなかったのでコメント書いた)

dungeon/roo.go
// CreateRoomsは、指定した区画内に9つの部屋を生成します。
func CreateRooms(width, height, minRoomSize, padding int) []*Room {
	rooms := make([]*Room, 9)
	for i := range rooms {
		rooms[i] = NewRoomInBounds(0, 0, width-1, height-1, func(opt *Option) {
			opt.MinRoomSize = minRoomSize
			opt.Padding = padding
		})
	}
	return rooms
}

9つの部屋は3x3に区切られて生成されて欲しいのでテストを修正する

dungeon/room_test.go
func TestCreateRooms(t *testing.T) {
	// Arrange
	width, height := 15, 12
	minRooSize := 2
	padding := 1

	testcases := map[string]struct {
		index  int
		left   int
		top    int
		right  int
		bottom int
	}{
		"部屋1": {index: 0, left: 1, top: 1, right: 3, bottom: 2},
		"部屋2": {index: 1, left: 6, top: 1, right: 9, bottom: 2},
		"部屋3": {index: 2, left: 11, top: 1, right: 13, bottom: 2},
		"部屋4": {index: 3, left: 1, top: 5, right: 3, bottom: 6},
		"部屋5": {index: 4, left: 6, top: 5, right: 9, bottom: 6},
		"部屋6": {index: 5, left: 11, top: 5, right: 13, bottom: 6},
		"部屋7": {index: 6, left: 1, top: 9, right: 3, bottom: 11},
		"部屋8": {index: 7, left: 6, top: 9, right: 9, bottom: 11},
		"部屋9": {index: 8, left: 11, top: 9, right: 13, bottom: 11},
	}

	for name, tt := range testcases {
		t.Run(name, func(t *testing.T) {
			// Act
			sut := dungeon.CreateRooms(width, height, minRooSize, padding)

			// Assert
			assert.True(t, tt.left <= sut[tt.index].X && sut[tt.index].X <= tt.right)
			assert.True(t, tt.top <= sut[tt.index].Y && sut[tt.index].Y <= tt.bottom)
			assert.True(t, tt.left <= sut[tt.index].Right() && sut[tt.index].Right() <= tt.right)
		})
	}
}

実装も修正。テストが通るまでコメント追記していく。

dungeon/room.go
// CreateRoomsは、指定した区画内に9つの部屋を生成します。
// 生成される部屋は指定した区画内に重ならないように配置されます。
// 部屋のwidthは指定されたwidthを3分割することで決定されます。
// 部屋のheightは指定されたheightを3分割することで決定されます。
// 部屋はNewRoomInBounds関数を使用して生成されます。
func CreateRooms(width, height, minRoomSize, padding int) []*Room {
	rooms := make([]*Room, 0, 9)
	for y := 0; y < 3; y++ {
		for x := 0; x < 3; x++ {
			roomWidth := width / 3
			roomHeight := height / 3
			room := NewRoomInBounds(
				roomWidth*x,
				roomHeight*y,
				roomWidth*(x+1)-1,
				roomHeight*(y+1)-1,
				func(opt *Option) {
					opt.MinRoomSize = minRoomSize
					opt.Padding = padding
				},
			)
			rooms = append(rooms, room)
		}
	}
	return rooms
}
ぱんだぱんだ

部屋を通路でつなぐ

TODOリストは以下

  • 2つの部屋をつなぐ通路を生成できること
  • 通路をマップ配列に書き込めること
  • 外周の8つの部屋は環状に通路で繋がること
  • 中央の部屋はランダムな部屋と通路でつながること
このスクラップは2024/03/10にクローズされました