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

A Stab At Roguish Go Part 02

shindakun profile image Steve Layton Updated on ・4 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

Continuing Adventures

After we finished the last post I continued to mess with the code a little bit more. So, we're going to continue our short trip down the rogue-like way. This time we're hacking in a static map and some basic "collision detection". And on that note, let's jump right in and get started.

in action


Code Walkthrough

The first bit of our code is more or less the same. We altered our player struct to add the rune which stands for our player character. We'll have to cast to a string() when we emit the string to the screen.

package main

import (
  "fmt"
  "math/rand"
  "os"
  "time"

  "github.com/gdamore/tcell"
  "github.com/mattn/go-runewidth"
)

type player struct {
  r      rune
  x      int
  y      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

Get Mapping

Since we're using a static "map" we're just going to use a multidimensional array. For walls we're using # and the floor is ..

  mapp := [9][9]rune{
    {'#', '#', '#', '#', '#', '#', '#', '#', '#'},
    {'#', '.', '.', '.', '.', '.', '.', '.', '#'},
    {'#', '.', '.', '.', '.', '.', '.', '.', '#'},
    {'#', '.', '.', '.', '#', '.', '.', '.', '#'},
    {'#', '.', '.', '#', '#', '#', '.', '.', '#'},
    {'#', '.', '.', '.', '#', '.', '.', '.', '#'},
    {'#', '.', '.', '.', '.', '.', '.', '.', '#'},
    {'#', '.', '.', '.', '.', '.', '.', '.', '#'},
    {'#', '#', '#', '#', '#', '#', '#', '#', '#'},
  }

Colors

We have added a couple more colors which we'll use for the walls and the ground when we draw. Nothing too exciting, just a couple of browns and grey.

  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.ColorGrey).
    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.Clear()

Controls and Collision

Our controls section is still a work in progress. Due to the way that tcell.EventKey works we have to check tcell.KeyRune and ev.Rune(). This way we can see if we have received a normal letter key. We want to be able to use Vim keys to move around so for now, we have a bit of duplication in our code. Before that, though let's take a closer look at how we are achieving our basic collision detection.

r, _, _, _ := s.GetContent(player.x-1, player.y)
if r == '#' {

} else if player.x-1 >= 0 {
  player.x--
}

We are using GetContent() to get the rune that is currently in the location that the player wants to move to. If that is a wall rune the nothing happens. We could extend this in the future to allow for combat, opening doors, and other interactions.

For now, we're going to deal with the duplication. I'll take a closer look at the tcell code and see if there is another alternative.

  quit := make(chan struct{})
  go func() {
    for {
      x, y := s.Size()
      ev := s.PollEvent()
      switch ev := ev.(type) {
      case *tcell.EventKey:

        switch ev.Key() {
        case tcell.KeyRune:
          switch ev.Rune() {
          case 'h':
            r, _, _, _ := s.GetContent(player.x-1, player.y)
            if r == '#' {

            } else if player.x-1 >= 0 {
              player.x--
            }
          case 'l':
            r, _, _, _ := s.GetContent(player.x+1, player.y)
            if r == '#' {

            } else if player.x+1 < x {
              player.x++
            }
          case 'k':
            r, _, _, _ := s.GetContent(player.x, player.y-1)
            if r == '#' {

            } else if player.y-1 >= 0 {
              player.y--
            }
          case 'j':
            r, _, _, _ := s.GetContent(player.x, player.y+1)
            if r == '#' {

            } else if player.y+1 < y {
              player.y++
            }
          }
        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++
          }
        case tcell.KeyLeft:
          r, _, _, _ := s.GetContent(player.x-1, player.y)
          if r == '#' {

          } else if player.x-1 >= 0 {
            player.x--
          }
        case tcell.KeyUp:
          r, _, _, _ := s.GetContent(player.x, player.y-1)
          if r == '#' {

          } else if player.y-1 >= 0 {
            player.y--
          }
        case tcell.KeyDown:
          r, _, _, _ := s.GetContent(player.x, player.y+1)
          if r == '#' {

          } else if player.y+1 < y {
            player.y++
          }
        case tcell.KeyCtrlD:
          debug = !debug
        case tcell.KeyCtrlL:
          s.Sync()
        }
      case *tcell.EventResize:
        s.Sync()
      }
    }
  }()

The first part of our labeled loop hasn't changed.

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)
    }
    var color tcell.Style

Mapping

Now we're finally to our basic map drawing routine. As you can see it's a basic loop. We'll draw our map to the screen before our player character.

    for i := 0; i < 9; i++ {
      for j := 0; j < 9; j++ {
        if mapp[i][j] == '#' {
          color = grey
        }
        if mapp[i][j] == '.' {
            color = burlyWood
          }
        }
        emitStr(s, i+1, j+1, color, string(mapp[i][j]))
      }
    }

    emitStr(s, 0, 0, white, msg)

    emitStr(s, player.x, player.y, white, player.r)
    s.Show()
  }

  s.Fini()
}

Wrapping Up

And there we have it a nice simple addition to our original code and a sort of framework that we can build off if we want to. I am a bit tempted to work on this a bit long to see if I can add a simple system for adding creatures and items. I may start by seeing if we can simplify the movement code first - we'll see where the mood takes us.


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