Grogue: A Roguelike Tutorial in Go (Part 3)
Tags: roguelike, go, tutorial, gamedev
Published: 2023-10-27 20:58:50

---
title: Grogue: A Roguelike Tutorial in Go (Part 3)
published: true
description: Generating a Dungeon
tags: #roguelike #go #tutorial #gamedev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icmpoo6orxbiw3oduuqx.png
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-10-11 17:42 +0000
---

In Part 1, we create the dungeon as a large, empty room with walls around the edge. In this part, we'll modify our dungeon generation code to start by filling the entire map with walls and then carving out rooms and connecting them with tunnels.

Start by creating a structure we'll use to create our rooms. Add the following code to `level.go`:

```go
type RectangularRoom struct {
	X1 int
	Y1 int
	X2 int
	Y2 int
}

// Create a new RectangularRoom structure.
func NewRectangularRoom(x int, y int, width int, height int) RectangularRoom {
	return RectangularRoom{
		X1: x,
		Y1: y,
		X2: x + width,
		Y2: y + height,
	}
}
```

The constructor takes the x and y coordinates of the top-level corner and computes the bottom right corner based on the width and height parameters.

In order to create tunnels between rooms, we'll also need a function to calculate the center of the room.

```go
// Returns the tile coordinates of the center of the RectangularRoom.
func (r *RectangularRoom) Center() (int, int) {
	centerX := (r.X1 + r.X2) / 2
	centerY := (r.Y1 + r.Y2) / 2
	return centerX, centerY
}
```

To ensure that our rooms are surrounded by at least one layer of wall tile, we should create a function that returns the interior of the room:

```go
// Returns the tile coordinates of the interior of the RectangularRoom.
func (r *RectangularRoom) Interior() (int, int, int, int) {
	return r.X1 + 1, r.X2 - 1, r.Y1 + 1, r.Y2 - 1
}
```

Using this, we can modify our `createTiles()` function, but we also want to keep track of our rooms, so we should add a slice of Rooms to our Level structure first.

```go
type Level struct {
	Tiles []MapTile
	Rooms []RectangularRoom  // NEW
}
```

Then let's refactor `createTiles()` to make a pair of rooms. (Don't worry, we'll get to procedural generation later.)

```go
func (level *Level) createTiles() {
	gd := NewGameData()
	tiles := make([]MapTile, gd.ScreenHeight*gd.ScreenWidth)

	// Fill with wall tiles
	for x := 0; x < gd.ScreenWidth; x++ {
		for y := 0; y < gd.ScreenHeight; y++ {
			idx := GetIndexFromCoords(x, y)
			wall, err := NewTile(x*gd.TileWidth, y*gd.TileHeight, TileWall)
			if err != nil {
				log.Fatal(err)
			}
			tiles[idx] = wall
		}
	}
	level.Tiles = tiles

	room1 := NewRectangularRoom(25, 15, 10, 15)
	room2 := NewRectangularRoom(40, 15, 10, 15)
	level.Rooms = append(level.Rooms, room1, room2)

	for _, room := range level.Rooms {
		x1, x2, y1, y2 := room.Interior()
		for x := x1; x <= x2; x++ {
			for y := y1; y <= y2; y++ {
				idx := GetIndexFromCoords(x, y)
				floor, err := NewTile(x*gd.TileWidth, y*gd.TileHeight, TileFloor)
				if err != nil {
					log.Fatal(err)
				}
				level.Tiles[idx] = floor
			}
		}
	}
}
```

Our first nested loops now fill the entire level with wall tiles. We then create new rooms and add them to the level's list of rooms. Finally, we iterate through all of the level's rooms and carve out their interiors by making them floor tiles. 

Unfortunately, our player isn't in a room anymore, so let's modify `NewGame()` in `main.go` to put the player in the center of the first room by changing the line where we create the player.

```go
	startX, startY := g.CurrentLevel.Rooms[0].Center()
	player, err := NewEntity(startX, startY, "player")
```

Run the game and you'll find the player in the first of two rooms.

![We have rooms!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k7scob7z8vcr2pw8ro7n.png)

## Tunneling Along 

That's cool and all, but the player is trapped in the first room. That just won't do. We need to create tunnels between rooms.

Let's start be creating a pair of private helper functions that will create vertical and horizontal tunnels.

```go
// Create a vertical tunnel.
func (level *Level) createVerticalTunnel(y1 int, y2 int, x int) {
	gd := NewGameData()
	for y := min(y1, y2); y < max(y1, y2)+1; y++ {
		idx := GetIndexFromCoords(x, y)

		if idx > 0 && idx < gd.ScreenHeight*gd.ScreenWidth {
			floor, err := NewTile(x*gd.TileWidth, y*gd.TileHeight, TileFloor)
			if err != nil {
				log.Fatal(err)
			}
			level.Tiles[idx] = floor
		}
	}
}

// Create a horizontal tunnel.
func (level *Level) createHorizontalTunnel(x1 int, x2 int, y int) {
	gd := NewGameData()
	for x := min(x1, x2); x < max(x1, x2)+1; x++ {
		idx := GetIndexFromCoords(x, y)

		if idx > 0 && idx < gd.ScreenHeight*gd.ScreenWidth {
			floor, err := NewTile(x*gd.TileWidth, y*gd.TileHeight, TileFloor)
			if err != nil {
				log.Fatal(err)
			}
			level.Tiles[idx] = floor
		}
	}
}
```

`createVerticalTunnel()` takes three arguments: a starting position on the Y-axis, an ending position on the Y-axis, and the X-coordinate. It then converts all tiles from `x, y1` to `x, y2` into floor tiles. `createHorizontalTunnel()` does the same thing, but on the X-axis instead of the Y.

We'll use both of these to tunnel from one room to another.

```go
// Tunnel from this first room to second room.
func (level *Level) tunnelBetween(first *RectangularRoom, second *RectangularRoom) {
	startX, startY := first.Center()
	endX, endY := second.Center()

	if rand.Intn(2) == 0 {
		// Tunnel horizontally, then vertically
		level.createHorizontalTunnel(startX, endX, startY)
		level.createVerticalTunnel(startY, endY, endX)
	} else {
		// Tunnel vertically, then horizontally
		level.createVerticalTunnel(startY, endY, startX)
		level.createHorizontalTunnel(startX, endX, endY)
	}
}
```

This private function takes two RectangularRooms as arguments and tunnels from one to the next. It generates a random number (don't forget to add `"math/rand"` to your list of imports!) and uses that to determine whether to first tunnel vertically or horizontally. It then utilizes those helper functions to do the actual tunneling.

We can now use this function inside `createTiles()` to connect the two rooms we made.

```go
	// Carve out rooms
	for roomNum, room := range level.Rooms {  // NEW
		x1, x2, y1, y2 := room.Interior()
		for x := x1; x <= x2; x++ {
			for y := y1; y <= y2; y++ {
				idx := GetIndexFromCoords(x, y)
				floor, err := NewTile(x*gd.TileWidth, y*gd.TileHeight, TileFloor)
				if err != nil {
					log.Fatal(err)
				}
				level.Tiles[idx] = floor
			}
		}
		if roomNum > 0 { // NEW
			level.tunnelBetween(&level.Rooms[roomNum-1], &level.Rooms[roomNum])  // NEW
		}  // NEW
	}
```

We changed the start of the for loop from `for _, room` to `for roomNum, room` because we now need to know which room we're working on. After the room is carved, we then tunnel from this room, to the previous room (if there is a previous room).

If you run the game now, it should look like this.

![Secret Tunnel!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xo0v08tq56bkqnbb5edx.png)

## More Rooms

Now that our room and tunnel functions work, it's time to move on to the actual dungeon generation. It'll be fairly simple: place rooms one at a time, make sure they don't overlap, then connect them with tunnels.

To do that, we'll need a function to determine if two rooms overlap.

```go
// Determines if this room intersects with otherRoom.
func (r *RectangularRoom) IntersectsWith(otherRoom RectangularRoom) bool {
	return r.X1 <= otherRoom.X2 && r.X2 >= otherRoom.X1 && r.Y1 <= otherRoom.Y2 && r.Y2 >= otherRoom.Y1
}
```

We'll need a few more variables in `GameData` to determine the minimum and maximum size of the rooms as well as the maximum number of rooms one floor can have.

```go
type GameData struct {
	ScreenWidth  int
	ScreenHeight int
	TileWidth    int
	TileHeight   int
	MaxRoomSize  int  // NEW
	MinRoomSize  int  // NEW
	MaxRooms     int  // NEW
}

// Creates a new instance of the static game data.
func NewGameData() GameData {
	gd := GameData{
		ScreenWidth:  80,
		ScreenHeight: 50,
		TileWidth:    16,
		TileHeight:   16,
		MaxRoomSize:  10,  // NEW
		MinRoomSize:  6,   // NEW
		MaxRooms:     30,  // NEW
	}
	return gd
}
```

With these variables in place, modify `createTiles()` replacing

```go
	room1 := NewRectangularRoom(25, 15, 10, 15)
	room2 := NewRectangularRoom(40, 15, 10, 15)
	level.Rooms = append(level.Rooms, room1, room2)
```

with the following:

```go
	for i := 0; i <= gd.MaxRooms; i++ {
		// generate width and height as random numbers between gd.MinRoomSize and gd.MaxRoomSize
		width := rand.Intn(gd.MaxRoomSize-gd.MinRoomSize+1) + gd.MinRoomSize
		height := rand.Intn(gd.MaxRoomSize-gd.MinRoomSize+1) + gd.MinRoomSize

		xPos := rand.Intn(gd.ScreenWidth - width)
		yPos := rand.Intn(gd.ScreenHeight - height)

		newRoom := NewRectangularRoom(xPos, yPos, width, height)

		isOkay := true
		for _, room := range level.Rooms {
			// check through all existing rooms to ensure newRoom doesn't intersect
			if newRoom.IntersectsWith(room) {
				isOkay = false
				break
			}
		}

		if isOkay {
			level.Rooms = append(level.Rooms, newRoom)
		}
	}
```

This one is a bit complicated, so let's break it apart.

```
width := rand.Intn(gd.MaxRoomSize-gd.MinRoomSize+1) + gd.MinRoomSize
height := rand.Intn(gd.MaxRoomSize-gd.MinRoomSize+1) + gd.MinRoomSize
```

This uses `rand.Intn()` to generate random widths and heights between `gd.MinRoomSize` (if `rand.Intn()` returns 0) and `gd.MaxRoomSize` (if `rand.Intn()` returns it's [maximum value](https://pkg.go.dev/math/rand#Intn), which is gd.MaxRoomSize-gd.MinRoomSize in this case).

```go
xPos := rand.Intn(gd.ScreenWidth - width)
yPos := rand.Intn(gd.ScreenHeight - height)
```

We then do a similar thing to grab the x- and y-coordinates of the top-left corner of the room, but ensure that our room doesn't extend off the edge of the map.

```go
newRoom := NewRectangularRoom(xPos, yPos, width, height)

isOkay := true
for _, room := range level.Rooms {
	// check through all existing rooms to ensure newRoom doesn't intersect
	if newRoom.IntersectsWith(room) {
		isOkay = false
		break
	}
}
```

After creating a room with the new random specifications, we loop through all of the previously placed rooms and see if this new room intersects with them. If it does, we flag this room as not okay and stop the loop.

```go
if isOkay {
	level.Rooms = append(level.Rooms, newRoom)
}
```

If we made it through the whole list without finding an intersection, we then add the room to the level's list of rooms.

Not too bad, right?

If you run the game now, you should see something similar to the following (but not the same, because it's random).

![Random Dungeon](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zm7a2esey2h64veppmkf.png)

That's it! We have a functioning dungeon generation algorithm.

You can view the complete source code [here](https://github.com/seancallaway/Grogue/tree/part03) and if you have any questions, please feel free to ask them in the comments.