Fat Old Yeti

Fat Old Yeti

Being a blog of thoughts and tutorials from a hobby game developer.

08 Feb 2021

Roguelike Tutorial 6

Roguelike in Go - Part 6 (Corridors)

All of the code for this tutorial can be found here.

Now that we have rooms, and our player is appropriately spawning in one, let’s connect them into a real dungeon map. All changes for this tutorial will be in level.go.

The first thing we will do is fix a small bug introduced in the last tutorial. The calls to get the x and y values in GenerateLevelTiles were not working quite as I expected them to upon several runs. Therefore, let’s change it from this:

x := GetDiceRoll(gd.ScreenWidth-w-1) - 1
y := GetDiceRoll(gd.ScreenHeight-h-1) - 1

To this:

x := GetDiceRoll(gd.ScreenWidth  -  w - 1)
y := GetDiceRoll(gd.ScreenHeight  -  h - 1)

That additional -1 was causing some issues.

Next, I was surprised to find out that Golang seems to have no Min and Max function. So let’s create them for our ease of use:

// Max returns the larger of x or y.
func max(x, y int) int {
	if x < y {
		return y
	}
	return x
}

// Min returns the smaller of x or y.
func min(x, y int) int {
	if x > y {
		return y
	}
	return x
}

Next, we will write a function which will carve a tunnel between two given points horizontally. I think everything in this function is pretty self-evident as to what’s going on.

func (level *Level) createHorizontalTunnel(x1 int, x2 int, y int) {
	gd := NewGameData()
	for x := min(x1, x2); x < max(x1, x2)+1; x++ {
		index := level.GetIndexFromXY(x, y)
		if index > 0 && index < gd.ScreenWidth*gd.ScreenHeight {
			level.Tiles[index].Blocked = false
			floor, _, err := ebitenutil.NewImageFromFile("assets/floor.png")
			if err != nil {
				log.Fatal(err)
			}
			level.Tiles[index].Image = floor
		}
	}
}

We also want to do the same thing for the vertical tunnels:

func (level *Level) createVerticalTunnel(y1 int, y2 int, x int) {
	gd := NewGameData()
	for y := min(y1, y2); y < max(y1, y2)+1; y++ {
		index := level.GetIndexFromXY(x, y)

		if index > 0 && index < gd.ScreenWidth*gd.ScreenHeight {
			level.Tiles[index].Blocked = false
			floor, _, err := ebitenutil.NewImageFromFile("assets/floor.png")
			if err != nil {
				log.Fatal(err)
			}
			level.Tiles[index].Image = floor
		}
	}
}

All that is left is some changes to the GenerateLevelTiles function: At the top of the function, where we are declaring the variables, add this one:

contains_rooms := false

For the rest of our changes, look for this code within the function:

if okToAdd {
	level.createRoom(new_room)

We will be putting all changes inside this block, below the CreateRoom call. Change this block so that it looks like the following:

if okToAdd {
	level.createRoom(new_room)
	if len(level.Rooms) == 0 {
		if contains_rooms {
			newX, newY := new_room.Center()
			prevX, prevY := level.Rooms[len(level.Rooms)-1].Center()

			coinflip := GetDiceRoll(2)

			if coinflip == 2 {
				level.createHorizontalTunnel(prevX, newX, prevY)
				level.createVerticalTunnel(prevY, newY, newX)
			} else {
				level.createHorizontalTunnel(prevX, newX, newY)
				level.createVerticalTunnel(prevY, newY, prevX)
			}
		}
	level.Rooms = append(level.Rooms, new_room)
	contains_rooms = true
	}
}

Ok, yeah. This is kind of nuts to look at. Let’s address it and explain what’s going on here. Each time we create a room, if its not the first room, we will determine if we can add a hallway between it and another room. We will always tunnel from center to center, so we grab those. Then, depending on the coinflip, we create the tunnels. This gives us a bit more randomness in our tunnels, giving the level some character.

Your screen should now have a complete map:

map
.

As always, if you have any questions, feel free to contact me at fatoldyeti@gmail.com or @idiotcoder on the gophers slack.