Tuesday, May 1, 2018

The Command Line as a Gaming Platform: ANSI Graphics and Midi Music

Introduction


Last September (2017) I got the bright idea that it would be fun to make a game for the command line. I wanted it to be graphical, but only using basic text characters (charmap is my good friend).  I messed around with it a bit in .NET and posted some experimental code to GitHub, and got as far as generating box drawings based on text files:



.NET has a handy Console class that makes moving the cursor around and changing text color very simple.  At the time I really didn't have the time or inclination to pursue the idea further (I was juggling job interview prep, discrete math, and finishing the 70-487 guide).

Fast forward six months. I'm at work trying to customize my default terminal prompt, when I find this handy dandy article about setting Bash Color and Formatting.  I play around with it a little bit in the shell, and the thought occurs to me "I wonder if you can use these in Java?"


Ascii art generation (intro to ANSI control codes)


Naturally my first stop of StackOverflow, and while I can't remember where I first saw it, I quickly figured out that the unicode literal sequence was the bridge between what I saw in the bash strings and what I want in Java.  So where in shell setting the foreground to red looks like this:

echo -e "\e[41m Some red text..."

In Java this would be:

System.out.println("\u001b[41m Some red text...");

Now I remembered playing around with that handy Console class in .NET, which kind of spoiled me a little bit.  It was just so easy to use, and Java really doesn't have anything equivalent in the standard library.  I began experimenting with 256 color a bit (Java Color Console Demo on GitHub), and thought it might be fun to upgrade to color ascii art generation.

Before I could start generating ascii art from image files, though, I had another problem I had to solve first: reading rgb data from an image file!  Luckily StackOverflow to the rescue as usual (color of pixels in buffered image).  At first I just sampled the pixels and translated the rgb value into a colored @, or block, or triangle...:



But it didn't really feel like proper "ascii art"... more like a textified jpeg artifact. Blech. So I came up with the idea of mapping groups of pixels to a "template" for a character and building up the image that way.  The fact that I got to use squared error as a selection criteria made me feel super smug and nerdy, and it wasn't too bad, though it was slow as hell.  I think it was a promising direction, but a hyper sophisticated ascii art engine wasn't really my goal.  So after a couple iterations of Rainbow Dash, I decided it was time to see what else I could do with ANSI escape codes.  This is when I found the wikipedia page that set me free.




It lives! (Moving the cursor)


The first thing I wanted to do was see the ANSI cursor move commands work in a terminal, so I took what I found in the Bash color and format article and applied it to what I found in the wikipedia page. So if I want to move the cursor to row 20, column 60, I can use the command:

echo -en "\e[20;60H"

I wanted a better abstraction for "move to an absolute position on the terminal screen", so I created a class with a method moveTo(row, col) that wraps this up nicely.  I wanted an interactive demo, but now I had a new problem.  It turns out that in the terminal window, input is generally "buffered", so you can't just read a raw keypress, you have to wait until Enter is pressed and the buffer is flushed.  I vaguely remember reading somewhere (probably SO) that .NET gets around this with some native OS integration, but with Java I wasn't so lucky.  It turns out you can actually change this in the terminal itself, so rather than venture down a rabbit hole, I created a shell script that calls stty before and after calling Java in order to toggle the input mode.

So now that I could accept single key presses other than [ENTER] from StdIn, I wanted to make the cursor move around.  So I filled the screen with hashes, and whenever the cursor moved, I replaced that location with a space.  Leaving a trail of spaces through a sea of hash.  It occurred to me this was a bit like an etch-a-sketch, and my "reset" function was sort of like "shaking it".  


I felt like it was time to take a stab at an actual playable game.  I had to shave several yaks to get there:  the keyboard listening code got factored into a class, the GameConsole class appeared that would evolve to become the overall coordinator, the Glyph class representing a character and its various ANSI formatting.  I dug out my .NET code for the box border generator and ported it to Java (seemed like it could be handy).

Eventually, I managed a passable version of Snake, where the player is a snake that runs around the screen eating fruit while avoiding the poison. The basic game mechanics are super simple to implement, so it was a good starter game to get some of the groundwork laid for later work.  One of the big leaps for Snake was getting sound working.




Beep Boop (Sound effects and music)


I appreciate that good sound can really bring a game to life.  It turns out that really basic sound playback is actually pretty easy in Java.  Load a .wav file, wire up a few classes from javax.sound.sampled, and you are all set.  Where it got tricky for me was that I was playing a sound file over and over (every game loop iteration), and after a few dozen loops (really just a few seconds), the sound crashed (if not the whole program).  StackOverflow to the rescue, just needed to do a better job with resource management. For good measure, to avoid reloading from disk hundreds of times, I cached the byte[] representing the file stream in memory, so after one load from disk everything else was "free".

Sadly, this was the easy bit.  I didn't have much trouble getting Midi playback working, however I thought "you know, this seems loud, I should make the music less obtrusive by lowering the volume". Which caused me to embark on an hours long quest to dynamically adjust the volume of a Midi stream.  I followed every suggestion I could find online, but I never did better than turning down the volume on a single channel.  

I eventually game up on volume control for Midi and let it go.  At some point I created "attribution.txt" files so that I could list where I found stuff.  It was ostensibly "free", and some specified that they wanted attribution, so it seemed like a safe CYA move. The sounds I get from freesound.org and the midi from www.midiworld.com and freemidi.org.


Load All the Games! (with Reflection)


As Snake started to progress, I found it annoying that dying would exit to the console. My first hack solution was to just have the game play five times before exiting, but I knew I wanted a title menu anyway, so I started working on the title screen.  I wanted the list of games to be generated dynamically based on what Java could see at runtime, and I just assumed that something like this was baked in (or at least easily implementable).  The idea was that one could code up a game outside of the Term Games project and just drop the jar on the classpath.  I'd seen it done in .NET (noticing a theme here?), so how hard could it be?

The initial returns were not promising, fortunately it didn't take me long to stumble across a class written in pure Java that would work nicely (I just had to fix up the generics cause it was really, really old Java lol).  All the games for Term Games are expected to subclass the Game abstract class, so I just enumerate all the loaded classes that are a subclass of Game.  I coded up a "Circular List" because I wanted to be able to loop around the list without having to think about it, which lended itself very nicely to implementing the spinner widget.

Some simple color manipulation and a bit of generated text art and bam, halfway decent title screen.


The games are all instantiated at load time, and because the same instance is always the one being played it isn't difficult to carry over state from previous play (often unintentionally).  While I could load the games lazily, I just don't see that being a big problem at the moment.



The Event Bus (or "Type Erasure is the Devil")


I knew that I wanted my code to be as decoupled as possible, and one mechanism I've used to that end in the past is an event bus (GWT used it pretty heavily, may it rest in peace).  I figured a generic event bus would be a pretty straight forward implementation, unfortunately Java generics always make things difficult.

The concept is simple enough: map a Class<K> to an EventListener<K>, where K extends Event. Nice and tight. Something like this:


 
private Map<Class<K>, List<EventListener<K>>> map = new HashMap<>();
 

But you can't do this without adding the type parameter to the EventBus class, which defeats the whole purpose, since it should be able to distribute any event type.  Replacing K with ? is legal, but then you could map an EventListener<B> to a Class<A> and the code wouldn't complain until runtime (EXCEPTION!).  The subscribe and unsubscribe methods I could add a type parameter to, so I could make sure that Class<?> and EventListener<?> actually both were parameterized by the same type K, and that K extends Event. But it got hairy trying to manipulate the map.  I eventually widdled it down to one unchecked cast, but I never felt fully satisfied with it.

However kludgy it ended up, it has managed to function reasonably well thus far.  I didn't want the Game classes to have to muck around with the SoundPlayer or Keyboard driver classes, I wanted a simple pub/sub mechanism that would abstract all that away.  So the Game can subscribe to KeyDownEvent and not have to worry about what the Keyboard driver is doing, and it can fire PlaySoundEvent to play a sound and not have to ever talk to the SoundPlayer directly.

One bit of wonkiness that I ran into that I didn't particularly like was all the bookkeeping involved to make sure that the handlers a game attached to the event bus went away when the game did.  After finishing Tetr... er... Quadtris I decided to have the abstract Game class handle subscribing and unsubscribing.  Ideally I wanted to be able to add handlers without the need to keep a reference around, and I wanted to be able to remove all the listeners for a game in one call (without just blowing all every listener on the event bus).

Java generics once again almost derailed my effort.  It was easy enough to add all the event listeners to a private list in Game and pass the Class and EventListener straight through to the event bus subscribe method, but unsubscribe expected the same information and I didn't want to have to basically reimplement the event bus logic to get around it.  Since I had the type information when making the call to subscribe, I had the idea of keeping a list of lambdas that do nothing but call unsubscribe with the right arguments.  Then when it's time to unsubscribe everybody, I just loop through the list of lambdas and call them all.


 
private final List<Consumer<Void>> listenerRemovers = new ArrayList<>();
 
final public <K extends Event> void addListener(Class<K> event,
                                                EventListener<K> listener){
    if(eventBus == null){
        return;
    }
    listenerRemovers.add((Void) -> {eventBus.unsubscribe(event, listener);});
    eventBus.subscribe(event, listener);
}
 
final public void removeLocalListeners(){
    for(Consumer<Void> lambda : listenerRemovers){
        lambda.accept(null);
    }
    listenerRemovers.clear();
}
 


So code that used to look like this:

 
private EventListener<KeyDownEvent> keyListener;
private EventListener<ToggleThemeEvent> themeListener;
private EventListener<FullRowsEvent> fullRowsListener;
private EventListener<RedrawPieceEvent> redrawPieceListener;
private EventListener<SpawnPieceEvent> spawnPieceListener;


private void removeListeners() {
    logger.debug("Removing SnakeGame listeners.");
    eventBus.unsubscribe(KeyDownEvent.class, keyListener);    
    eventBus.unsubscribe(ToggleThemeEvent.class, themeListener);
    eventBus.unsubscribe(FullRowsEvent.class, fullRowsListener);
    eventBus.unsubscribe(RedrawPieceEvent.class, redrawPieceListener);
    eventBus.unsubscribe(SpawnPieceEvent.class, spawnPieceListener);
}

private void initializeListeners() {
    logger.debug("Initializing SnakeGame listeners.");
    keyListener = (KeyDownEvent k) -> {handleKeyDownEvent(k);};
    themeListener = (ToggleThemeEvent tte) -> {handleToggleThemeEvent(tte);};
    fullRowsListener = (FullRowsEvent fre) -> {handleFullRowsEvent(fre);};
    redrawPieceListener = (RedrawPieceEvent rpe) -> {handleRedrawPieceEvent(rpe);};
    spawnPieceListener = (SpawnPieceEvent spe) -> {handleSpawnPieceEvent(spe);};
    eventBus.subscribe(KeyDownEvent.class, keyListener);
    eventBus.subscribe(ToggleThemeEvent.class, themeListener);
    eventBus.subscribe(FullRowsEvent.class, fullRowsListener);
    eventBus.subscribe(RedrawPieceEvent.class, redrawPieceListener);
    eventBus.subscribe(SpawnPieceEvent.class, spawnPieceListener);
}
 


Now looks like this:


 
private void removeListeners() {
    logger.debug("Removing SnakeGame listeners.");
    removeLocalListeners();
}

private void initializeListeners() {
    logger.debug("Initializing SnakeGame listeners.");
    addListener(KeyDownEvent.class, 
        (KeyDownEvent k) -> {handleKeyDownEvent(k);});
    addListener(ToggleThemeEvent.class, 
        (ToggleThemeEvent tte) -> {handleToggleThemeEvent(tte);});
    addListener(FullRowsEvent.class, 
        (FullRowsEvent fre) -> {handleFullRowsEvent(fre);});
    addListener(RedrawPieceEvent.class, 
        (RedrawPieceEvent rpe) -> {handleRedrawPieceEvent(rpe);});
    addListener(SpawnPieceEvent.class, 
        (SpawnPieceEvent spe) -> {handleSpawnPieceEvent(spe);});
}
 

Much less fuss =)


The Game Loop (or "Concurrency is the Devil")



One bug that vexed me throughout the development of Quadtris was the intermittent wonky rendering.  You'd drop a piece, and a phantom image of it would be left where it used to be.  For most of the development I chalked it up to imperfect rendering.

However, when I factored out all the game logic into a separate class, the problems exploded.  Now it wasn't just phantom graphical artifacts.  You'd drop a piece, and it's like it spawned an evil clone and dropped them instead.  Two or three times in a row sometimes.  It was weird.  I suspected the weird state problems were due to concurrency bugs.  Events were coming in from the Keyboard driver from another thread, and I figured that they were probably stepping on each other or on the tick() call coming from the main game loop.  So I synchronized all the mutation methods on the game state class (called it a "board", because I couldn't think of anything better).

What I didn't expect was that it would fix the rendering problems too. I had already tried to fix the graphic issued by synchronizing calls to the renderer.  This prevented weirdness where one call would move the cursor, then a second call would move it again, and the first one would start writing in the seconds location. But apparently what I didn't realize was happening is that events were coming in and changing the underlying state, so the renderer was drawing the wrong thing the right way.  One of those rare moments where happenstance works in your favor.

Using synchronized all over the place seemed a bit ham-fisted, but with as light as the load is elsewhere I think I can afford the additional overhead.  Better to be correct.


The Results so far...




I'll admit I'm pretty proud of myself.  My Tetris clone turned out even better than I expected.  I have ideas for a number of other games that would be of similar complexity, mostly retro clones.  I also have a few ideas that are less knock-off-ish.  So it should be insteresting, at least until I get tired of it.  My code is all on GitHub: TerminalGames.

No comments:

Post a Comment