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.
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.
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
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()
}
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()
}
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.
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 (3)
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