A Stab At Roguish Go Part 01
Tags: go, gamedev, roguelike, atlg
Published: 2019-03-30 18:14:01

---
title: A Stab At Roguish Go Part 01
published: true
description: "With Nethack and ADOM on the brain we trip into the dungeons."
tags: go, gamedev, roguelike, atlg
cover_image: https://dev.shindakun.net/img/attemptinggo_w.png
series: "ATLG Rogue"
---

# 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"](https://itch.io/jam/7drl-challenge-2019) competition. With roguelikes, [Nethack](https://www.nethack.org/) and [ADOM](https://www.adom.de/home/index.html) in particular, on the brain lately, it was a short jump.

![BAM Screenshot](https://thepracticaldev.s3.amazonaws.com/i/uiwt9gcsf8hini7s4ycq.gif)

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*](https://en.wikipedia.org/wiki/Beneath_Apple_Manor) was the first commercial roguelike game. It landed some two years before the genre's namesake - [*Rogue*](https://en.wikipedia.org/wiki/Rogue_(video_game)). 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](https://thepracticaldev.s3.amazonaws.com/i/ko9r2dv6mt65uxdare81.PNG)

***

We're not going to write an entire system to draw to the console. Instead, we'll start with [`tcell`](https://github.com/gdamore/tcell/) and use it as our base. There is an old post over on [Roguebasin](http://roguebasin.roguelikedevelopment.org/index.php?title=How_to_Write_a_Roguelike_in_15_Steps) 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`.

```bash
➜ 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](https://thepracticaldev.s3.amazonaws.com/i/npl3bbc2fek5tcmbviqk.png)

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.

```go
  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.

```go
  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.

```go
  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.

```go
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.

```go
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()`.

```go
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.

```go
  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.

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

  s.Fini()
}
```

![Where we at?](https://thepracticaldev.s3.amazonaws.com/i/hropulhlcletftbvh03i.png)

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

```go
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.

```go
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`.

```go
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.

```go
  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.

```go
    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](https://thepracticaldev.s3.amazonaws.com/i/gqj3ayjnq9jop26ff256.gif)

***

## 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.

{% github shindakun/atlg %}

***

| Enjoy this post? |
|:----:|
| [How about buying me a coffee?](https://ko-fi.com/shindakun) |

***