ATLG Rogue
Populating Dungeons
I've been having fun not thinking about API's or tools so we're going to stick to our rogue-ish code a bit longer. Let's go back into the depths! This time around we're going to take the basic creature creating code and add it to our dungeon code. This should put us in a good place to begin adding some code to either begin actual combat or possibly first saving and loading the game state. We'll see I guess.
For now, though let's dive in. If you haven't already you may want to take at the code from last time.
Code Walkthrough
This will be slightly different than previous posts since I've decided to break the creature code out of main.go
to hopefully keep everything a bit more organized. Currently, the GitHub repo is a bit behind but, I'll try and get that updated since it might be easier to see all in one spot. We will also be moving the actor code up out of here soon as well.
main.go
We're starting off by adding our "die" rolling code and our newly creature
directory. Note that it is just a subdirectory of at
which is how it is structured on my hard drive, this may change in the future but for now, that's how it is.
package main
import (
"fmt"
"math/rand"
"os"
"time"
"github.com/gdamore/tcell"
"github.com/mattn/go-runewidth"
"github.com/shindakun/at/creatures"
"github.com/shindakun/die"
)
The basic idea I'm working with is having a slice of Actors
that we can loop through every time we move forward in game time. This code will likely be pulled up out of main as well and placed in its own directory. Actors
holds an Actor
this struct contains the basic information about that actor. It's internal ID, location (x,y), the "floor" it is on, and it's color. We also see our first use of creatures.Creature
, we'll look at that later.
// Actors struct contains slice of Actor
type Actors struct {
Actors []Actor
}
// Actor strcut
type Actor struct {
ID string
X int
Y int
Floor int
Color tcell.Style
Creature creatures.Creature
}
For now, we will just have actors move around at random. In the future, we'll add code for hostilities and maybe some specific code based on which actor
or creature
we have. I can probably pull the random seed up out of here and should really remove the panic so we're not crashing out... maybe next time.
// Move moves the current actor randomly
func (a *Actor) Move(floor int, s tcell.Screen) {
if floor == a.Floor {
/*
1
4+2
3
*/
rand.Seed(time.Now().UTC().UnixNano())
var xx, yy int
d, err := die.Roll("1d4")
if err != nil {
panic("die roll")
}
switch d {
case 1:
xx = -1
case 2:
yy = 1
case 3:
xx = 1
case 4:
yy = -1
}
l, _, _, _ := s.GetContent(a.X+xx, a.Y+yy)
if l == '#' {
} else {
a.X = a.X + xx
a.Y = a.Y + yy
}
}
}
Besides Move()
we have a few "utility" functions, Draw()
and GetLocation()
. They should be pretty obvious.
// Draw draws the current Actor to the screen
func (a *Actor) Draw(s tcell.Screen, f int) {
if f == a.Floor {
emitStr(s, a.X, a.Y, a.Color, string(a.Creature.GetRune()))
}
}
// GetLocation returns the coordinates of the current Actor
func (a *Actor) GetLocation() (x, y int) {
return a.X, a.Y
}
NewActor()
will return an actor we can add to our Actors
slice. Note that I'm not actually using the ID
yet. My thinking was I can use that to create named creatures within the slice. After this, we come back to some code similar to the second post.
// NewActor creates a new actor
func NewActor(x, y, f int, color tcell.Style, c creatures.Creature) Actor {
return Actor{
X: x,
Y: y,
Floor: f,
Color: color,
Creature: c,
}
}
type player struct {
r rune
x int
y int
health int
level int
}
func emitStr(s tcell.Screen, x, y int, style tcell.Style, str string) {
for _, c := range str {
var comb []rune
w := runewidth.RuneWidth(c)
if w == 0 {
comb = []rune{c}
c = ' '
w = 1
}
s.SetContent(x, y, c, comb, style)
x += w
}
}
func main() {
debug := false
player := player{
r: '@',
x: 3,
y: 3,
}
var msg string
mapp := [9][9]rune{
{'#', '#', '#', '#', '#', '#', '#', '#', '#'},
{'#', '.', '.', '.', '.', '.', '.', '.', '#'},
{'#', '.', '.', '.', '.', '.', '.', '.', '#'},
{'#', '.', '.', '.', '#', '.', '.', '.', '#'},
{'#', '.', '.', '#', '#', '#', '.', '.', '#'},
{'#', '.', '.', '.', '#', '.', '.', '.', '#'},
{'#', '.', '.', '.', '.', '.', '>', '.', '#'},
{'#', '.', '.', '.', '.', '.', '.', '.', '#'},
{'#', '#', '#', '#', '#', '#', '#', '#', '#'},
}
mapp2 := [9][9]rune{
{'#', '#', '#', '#', '#', '#', '#', '#', '#'},
{'#', '#', '.', '.', '.', '.', '.', '#', '#'},
{'#', '.', '.', '.', '.', '.', '.', '.', '#'},
{'#', '.', '.', '.', '.', '.', '.', '.', '#'},
{'#', '.', '.', '.', '#', '.', '.', '.', '#'},
{'#', '.', '.', '.', '.', '.', '.', '.', '#'},
{'#', '.', '.', '.', '.', '.', '<', '.', '#'},
{'#', '#', '.', '.', '.', '.', '.', '#', '#'},
{'#', '#', '#', '#', '#', '#', '#', '#', '#'},
}
level := 1
current := mapp
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
s, e := tcell.NewScreen()
if e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(1)
}
if e = s.Init(); e != nil {
fmt.Fprintf(os.Stderr, "%v\n", e)
os.Exit(1)
}
white := tcell.StyleDefault.
Foreground(tcell.ColorWhite).
Background(tcell.ColorBlack)
grey := tcell.StyleDefault.
Foreground(tcell.ColorGray).
Background(tcell.ColorBlack)
burlyWood := tcell.StyleDefault.
Foreground(tcell.ColorBurlyWood).
Background(tcell.ColorBlack)
brown := tcell.StyleDefault.
Foreground(tcell.ColorBrown).
Background(tcell.ColorBlack)
s.SetStyle(tcell.StyleDefault.
Foreground(tcell.ColorWhite).
Background(tcell.ColorBlack))
s.EnableMouse()
s.Clear()
We're going to just build a handful of actors to populate the two levels we have right now. Soon we'll build out some level generation code and have it populated during that step. After this, we move into our input Go routine.
a := &Actors{}
a.Actors = append(a.Actors, NewActor(4, 3, 1, brown, &creatures.Pig{R: 'p', Health: 10, Description: "From the realm of Paradox... the Pig."}))
a.Actors = append(a.Actors, NewActor(5, 2, 1, brown, &creatures.Pig{R: 'p', Health: 10, Description: "Oink."}))
a.Actors = append(a.Actors, NewActor(4, 3, 2, brown, &creatures.Rat{R: 'r', Health: 10, Description: ""}))
a.Actors = append(a.Actors, NewActor(5, 2, 2, brown, &creatures.Rat{R: 'r', Health: 10, Description: ""}))
a.Actors = append(a.Actors, NewActor(4, 3, 2, brown, &creatures.Rat{R: 'r', Health: 10, Description: ""}))
a.Actors = append(a.Actors, NewActor(5, 2, 2, brown, &creatures.Rat{R: 'r', Health: 10, Description: ""}))
quit := make(chan struct{})
go func() {
for {
x, y := s.Size()
ev := s.PollEvent()
switch ev := ev.(type) {
case *tcell.EventKey:
// arrows/named keys
switch ev.Key() {
case tcell.KeyRune:
switch ev.Rune() {
Here we have a very quick stab at a "look" function, it isn't perfect but it will work for now. We follow that up with the ability to go up and down stairs. Note how we've added a basic for
loop for our actor movement. We might raise that out to its own function in the next revision of the code.
case ':':
g := current[player.x-1][player.y-1]
if g == '.' {
msg = "You see some dirt."
} else if g == '>' {
msg = "You see some stairs leading down."
} else if g == '<' {
msg = "You see some stairs leading up."
}
case '>':
g := current[player.x-1][player.y-1]
if g == '>' {
level++
s.Clear()
}
case '<':
g := current[player.x-1][player.y-1]
if g == '<' {
level--
s.Clear()
}
case 'h':
r, _, _, _ := s.GetContent(player.x-1, player.y)
if r == '#' {
} else if player.x-1 >= 0 {
player.x--
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
case 'l':
r, _, _, _ := s.GetContent(player.x+1, player.y)
if r == '#' {
} else if player.x+1 < x {
player.x++
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
case 'k':
r, _, _, _ := s.GetContent(player.x, player.y-1)
if r == '#' {
} else if player.y-1 >= 0 {
player.y--
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
case 'j':
r, _, _, _ := s.GetContent(player.x, player.y+1)
if r == '#' {
} else if player.y+1 < y {
player.y++
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
}
case tcell.KeyEscape, tcell.KeyEnter:
close(quit)
return
case tcell.KeyRight:
r, _, _, _ := s.GetContent(player.x+1, player.y)
if r == '#' {
} else if player.x+1 < x {
player.x++
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
case tcell.KeyLeft:
r, _, _, _ := s.GetContent(player.x-1, player.y)
if r == '#' {
} else if player.x-1 >= 0 {
player.x--
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
case tcell.KeyUp:
r, _, _, _ := s.GetContent(player.x, player.y-1)
if r == '#' {
} else if player.y-1 >= 0 {
player.y--
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
case tcell.KeyDown:
r, _, _, _ := s.GetContent(player.x, player.y+1)
if r == '#' {
} else if player.y+1 < y {
player.y++
}
for i := range a.Actors {
a.Actors[i].Move(level, s)
}
case tcell.KeyCtrlD:
debug = !debug
case tcell.KeyCtrlL:
s.Clear()
s.Sync()
}
case *tcell.EventResize:
s.Sync()
}
}
}()
loop:
for {
select {
case <-quit:
break loop
case <-time.After(time.Millisecond * 50):
}
// s.Clear()
dbg := fmt.Sprintf("player x: %d y: %d", player.x, player.y)
if debug == true {
var yy int
if player.y == 0 {
_, yy = s.Size()
yy--
} else {
yy = 0
}
emitStr(s, 0, yy, white, dbg)
}
Before we render anything to the screen we need to make sure we are accounting for the level that we are currently on. This should become a bit nicer and not just an if
statement once generation code is in place - and I decide how to store everything.
if level == 1 {
current = mapp
} else if level == 2 {
current = mapp2
}
// draw "map"
var color tcell.Style
for i := 0; i < 9; i++ {
for j := 0; j < 9; j++ {
if current[i][j] == '#' {
color = grey
}
if current[i][j] == '.' {
color = burlyWood
}
emitStr(s, i+1, j+1, color, string(current[i][j]))
}
}
emitStr(s, 0, 0, white, msg)
for i := range a.Actors {
a.Actors[i].Draw(s, level)
}
emitStr(s, player.x, player.y, white, string(player.r))
s.Show()
}
s.Fini()
}
That brings us down through our main
now we'll take a look at the imported creatures directory. I have a couple of different creatures but they are more or less the same thing at this point so I won't include them all. The rest will be up on GitHub soon-ish.
First, we have creatures.go
. This may not be the best way to handle this but I've decided to give it a go and see how it works out. Every creature will satisfy an interface. For now, that means they must have four functions. They are all pretty self-explanatory as you can see.
creatures/creatures.go
package creatures
type Creature interface {
GetRune() rune
GetHealth() int
GetDescription() string
TakeDamage(int)
}
creatures/pig.go
The immediate drawback of this interface style of handling creatures is all of them implement more or less the same code for now. Which kind of makes me want to scrap it and rework how it's done. Well, maybe after we figure put map generation or combat. Again, the code is pretty easy to follow we are implementing functions to satisfy our interface. This allows us to call the creature and the function we need from the said creature. p.GetRune
returns 'p'
for instance.
package creatures
// Pig struct
type Pig struct {
R rune `json:"r,omitempty"`
Health int `json:"health,omitempty"`
Description string `json:"description,omitempty"`
}
// GetRune returns rune
func (p *Pig) GetRune() rune {
return p.R
}
// GetHealth returns int
func (p *Pig) GetHealth() int {
return p.Health
}
// GetDescription returns string
func (p *Pig) GetDescription() string {
return p.Description
}
// TakeDamage applies damage to health
func (p *Pig) TakeDamage(i int) {
p.Health = p.Health - i
}
Wrapping Up
That about does it for this time around, literally, I am beat. I think I'm coming down with something so we'll wrap up for now. Let me know in the comments if you have any questions or if something isn't clear.
You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.
shindakun / atlg
Source repo for the "Attempting to Learn Go" posts I've been putting up over on dev.to
Attempting to Learn Go
Here you can find the code I've been writing for my Attempting to Learn Go posts that I've been writing and posting over on Dev.to.
Post Index
Enjoy this post? |
---|
How about buying me a coffee? |
Top comments (0)