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

A Stab At Roguish Go Part 01

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

Uhh OK

I wanted to take a break from looking at APIs and the like, but couldn't quite decide what to do all week. The answer came a day or so ago when I saw a post about the judging of the "7 Day Rogue Like" competition. With roguelikes, Nethack and ADOM in particular, on the brain lately, it was a short jump.

BAM Screenshot

But, first a quick history lesson for anyone who has no clue what I'm rambling about. Back before computers had powerful graphics processors text-based games were all the rage. 1978's Beneath Apple Manor was the first commercial roguelike game. It landed some two years before the genre's namesake - Rogue. The gist is you, as the player, must delve into a dungeon to retrieve a powerful artifact. Battle monsters, find weapons and upgrades and die.

Rogue Screenshot


We're not going to write an entire system to draw to the console. Instead, we'll start with tcell and use it as our base. There is an old post over on Roguebasin called How To Write a Roguelike in 15 Steps. We are going to do steps one through three.


Getting Started

The first thing we need to do is clone the tcell repo from GitHub. After that, we navigate to the _demos directory. Oh, and we must not forget to actually go get tcell.

➜ git clone https://github.com/gdamore/tcell.git
➜ cd tcell/_demos
➜ _demos git:(master) ✗ go get github.com/gdamore/tcell
➜ _demos git:(master) ✗ go run boxes.go

Boxes Screenshot

The repo includes four working examples that we can begin trying to pick apart. All four are good examples of a nice simple program loop. Here's a small snippet from the main() in boxes.go. First, it's setting up an ASCII character fallback, then creating a new screen. If any of that fails we exit with error code 1. Screen initialized, we can then set our style, in this case, a white background with black text. Finally, we clear the screen.

  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)
  }

  s.SetStyle(tcell.StyleDefault.
    Foreground(tcell.ColorBlack).
    Background(tcell.ColorWhite))
  s.Clear()

Simple right? Immediately after this, we create a quit channel and an anonymous goroutine. tcell.EventKey is going to take care of reading keyboard input. Perfect! We should be able to update this to move our player symbol around the screen.

  quit := make(chan struct{})
  go func() {
    for {
      ev := s.PollEvent()
      switch ev := ev.(type) {
      case *tcell.EventKey:
        switch ev.Key() {
        case tcell.KeyEscape, tcell.KeyEnter:
          close(quit)
          return
        case tcell.KeyCtrlL:
          s.Sync()
        }
      case *tcell.EventResize:
        s.Sync()
      }
    }
  }()

Now, we come to our main loop. It checks the quit channel to see if we should exit. If we don't, we run makebox(). We'll skip over that code since we don't need it.

  cnt := 0
  dur := time.Duration(0)
loop:
  for {
    select {
    case <-quit:
      break loop
    case <-time.After(time.Millisecond * 50):
    }
    start := time.Now()
    makebox(s)
    cnt++
    dur += time.Now().Sub(start)
  }

  s.Fini()

Real Starting Point

The first step on our journey is to copy and paste the code from boxes.go into a new file. I used at.go. Then we will trim it down to what we need. If we were to run the code we'll see that we get a blank screen, until we hit escape.

package main

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

  "github.com/gdamore/tcell"
)

func main() {
  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)
  }

  s.SetStyle(tcell.StyleDefault.
    Foreground(tcell.ColorBlack).
    Background(tcell.ColorWhite))
  s.Clear()

  quit := make(chan struct{})
  go func() {
    for {
      ev := s.PollEvent()
      switch ev := ev.(type) {
      case *tcell.EventKey:
        switch ev.Key() {
        case tcell.KeyEscape, tcell.KeyEnter:
          close(quit)
          return
        case tcell.KeyCtrlL:
          s.Sync()
        }
      case *tcell.EventResize:
        s.Sync()
      }
    }
  }()

loop:
  for {
    select {
    case <-quit:
      break loop
    case <-time.After(time.Millisecond * 50):
    }

    // makebox(s)
  }

  s.Fini()
}

Printing To The Screen

We have a good start but let's hack in the ability to print to screen. Again, I jumped back into the demo code. Inside mouse.go we have a nice example of how we can write text to the screen in emitStr(). I may take some time over the next week to read over the entire tcell codebase to get a bit more familiar with it. But for now, we'll borrow liberally from the examples. Note that we'll need to go get github.com/mattn/go-runewidth to take advantage of this function.

Anyway, we're going to range through our string and determine how wide the runes are one character at a time. Calculating the spacing we need as we go. Then we pass that and our style to SetContent(). This sets up what we are going to show on the screen but doesn't actually show anything. Show() or Sync() will handle that later.

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
  }
}

Where You At

Let's put it together and show our @ on the screen! We start by adding in emitStr().

package main

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

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

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() {
  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)
  }

We want to swap the foreground and background colors so we can keep a nice dark background. We also set up white to pass directly to our printing function.

  white := tcell.StyleDefault.
    Foreground(tcell.ColorWhite).
    Background(tcell.ColorBlack)

  s.SetStyle(tcell.StyleDefault.
    Foreground(tcell.ColorWhite).
    Background(tcell.ColorBlack))
  s.Clear()

  quit := make(chan struct{})
  go func() {
    for {
      ev := s.PollEvent()
      switch ev := ev.(type) {
      case *tcell.EventKey:
        switch ev.Key() {
        case tcell.KeyEscape, tcell.KeyEnter:
          close(quit)
          return
        case tcell.KeyCtrlL:
          s.Sync()
        }
      case *tcell.EventResize:
        s.Sync()
      }
    }
  }()

loop:
  for {
    select {
    case <-quit:
      break loop
    case <-time.After(time.Millisecond * 50):
    }

We Clear() the screen inside our loop. Then place our character. Finally, we call Show() to actually display it on screen.

    s.Clear()
    emitStr(s, 0, 0, white, "@")
    s.Show()
  }

  s.Fini()
}

Where we at?

And there we have it! But that's not good enough we can go further!


Baby Steps

I think this may be the most code listings I've put in a post. Hopefully, it doesn't make it difficult to read. Each is small enough I think they are digestible - let me know in the comments. Anyway, it's time for our little "adventurer" to take his or her first steps around a larger world (or black abyss as is the case currently).

package main

import (
  "fmt"
  "os"
  "time"

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

Our "player" is going to be a very simple struct that holds the X and Y coordinates of where we want the player to be. We'll use this to tell emitStr() where we are, updating it will allow us to move around the screen.

type player struct {
  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
  }
}

debug? We're going to add in a bit of code to print the current location of the player to the screen. While we are here we also initialize our player at 0,0.

func main() {
  debug := false
  player := player{
    x: 0,
    y: 0,
  }

  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)

  s.SetStyle(tcell.StyleDefault.
    Foreground(tcell.ColorWhite).
    Background(tcell.ColorBlack))
  s.Clear()

We've extended our input handler to take input from the arrow keys. We'll increment or decrement the player X and/or Y as needed depending on current location and key pressed. We've also added Ctrl-D which will flip our debug boolean allowing us to turn debug messages on and off.

  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.KeyEscape, tcell.KeyEnter:
          close(quit)
          return
        case tcell.KeyRight:
          if player.x+1 < x {
            player.x++
          }
        case tcell.KeyLeft:
          if player.x-1 >= 0 {
            player.x--
          }
        case tcell.KeyUp:
          if player.y-1 >= 0 {
            player.y--
          }
        case tcell.KeyDown:
          if player.y+1 < y {
            player.y++
          }
        case tcell.KeyCtrlD:
          debug = !debug
        case tcell.KeyCtrlL:
          s.Sync()
        }
      case *tcell.EventResize:
        s.Sync()
      }
    }
  }()

loop:
  for {
    select {
    case <-quit:
      break loop
    case <-time.After(time.Millisecond * 50):
    }
    s.Clear()

Here is our debug message code. We made sure that it takes into account the current row the player is on to either print on row 0 or the last row on the screen. We then draw our character and begin looping.

    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)
    }

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

  s.Fini()
}

moving about


Wrapping Up

It was fun to grab some code and quickly whip up a little concept program. I haven't decided if we're going to continue the journey through the "15 Steps" post or go back over to some sort of utility program. Let me know in the comments what you think.


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
 

we need such more posts. Great guide. Thanks Steve <3

 

Thanks for the comment! Maybe I will write a follow-up. I'm not sure how far I'll go but I did keep messing with the code.

 

this GIF reminds me of Secret life of objects (critters) in Javascript :D