loading...
Cover image for A Stab At Roguish Go Part 04

A Stab At Roguish Go Part 04

shindakun profile image Steve Layton ・8 min read

ATLG Rogue (4 Part Series)

1) A Stab At Roguish Go Part 01 2) A Stab At Roguish Go Part 02 3) A Stab At Roguish Go Part 03 4) A Stab At Roguish Go Part 04

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.



ATLG Rogue (4 Part Series)

1) A Stab At Roguish Go Part 01 2) A Stab At Roguish Go Part 02 3) A Stab At Roguish Go Part 03 4) A Stab At Roguish Go Part 04

Posted on by:

shindakun profile

Steve Layton

@shindakun

I've been known to write some code from time to time.

Discussion

markdown guide